Skip to content

Commit

Permalink
CORE: Add Test attribute
Browse files Browse the repository at this point in the history
CLI: Add test command
Tests: Add a tests project for testing language features
  • Loading branch information
MineGame159 committed Jan 14, 2024
1 parent e0fb44b commit 20a9ed1
Show file tree
Hide file tree
Showing 15 changed files with 501 additions and 9 deletions.
211 changes: 211 additions & 0 deletions cmd/cmd/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package cmd

import (
"bytes"
"errors"
"fireball/cmd/build"
"fireball/core/ast"
"fireball/core/ir"
"fireball/core/workspace"
"fmt"
"github.com/fatih/color"
"github.com/spf13/cobra"
"log"
"os/exec"
"strconv"
"strings"
"time"
)

func GetTestCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "test",
Short: "Test project.",
Run: testCmd,
}

return cmd
}

func testCmd(_ *cobra.Command, _ []string) {
start := time.Now()

// Create project
project, err := workspace.NewProject(".")
if err != nil {
log.Fatalln(err.Error())
}

// Load files
err = project.LoadFiles()
if err != nil {
log.Fatalln(err.Error())
}

// Report errors
build.Report(project)

// Build
tests := getTests(project)

output, err := build.Build(project, generateTestsEntrypoint(tests), 0, fmt.Sprintf("%s_tests", project.Config.Name))
if err != nil {
log.Fatalln(err.Error())
}

// Run
failed, err := runTests(output, tests)
if err != nil {
log.Fatalln(err.Error())
}

// Print info
took := time.Now().Sub(start)

namespaceStyle := color.New(color.FgWhite)
testStyle := color.New(color.Underline)

for _, test := range failed {
file := ast.GetParent[*ast.File](test)

for _, part := range file.Namespace.Name.Parts {
_, _ = namespaceStyle.Printf("%s.", part)
}

if s := test.Method(); s != nil {
_, _ = namespaceStyle.Printf("%s.", s.Name)
}

_, _ = testStyle.Print(test.TestName())

color.Red(" failed")
fmt.Println()
}

if len(failed) == 0 {
_, _ = color.New(color.FgGreen).Printf("%d tests succeeded", len(tests))
} else {
_, _ = color.New(color.FgYellow).Printf("%d out of %d tests failed", len(failed), len(tests))
}

fmt.Printf(", took %s\n", took)
}

// Run tests

func runTests(path string, tests []*ast.Func) ([]*ast.Func, error) {
// Run
cmd := exec.Command(path)

stdout := bytes.Buffer{}
cmd.Stdout = &stdout

err := cmd.Run()
if err != nil || !cmd.ProcessState.Success() {
return nil, errors.New("failed to run: " + path)
}

// Parse output
var failed []*ast.Func

for _, line := range strings.Split(stdout.String(), "\n") {
index, err := strconv.ParseInt(line, 10, 32)
if err != nil {
continue
}

failed = append(failed, tests[index])
}

return failed, nil
}

// Entrypoint generation

func generateTestsEntrypoint(tests []*ast.Func) *ir.Module {
m := &ir.Module{Path: "__entrypoint"}

// Run
printf := m.Declare("printf", &ir.FuncType{
Params: []*ir.Param{{
Typ: &ir.PointerType{Pointee: ir.I8},
Name_: "format",
}},
Variadic: true,
})

testType := &ir.FuncType{Returns: ir.I1}

testParam := &ir.Param{Typ: testType, Name_: "test"}
testIndexParam := &ir.Param{Typ: ir.I32, Name_: "index"}

run := m.Define("__run_test", &ir.FuncType{Params: []*ir.Param{testParam, testIndexParam}}, 0)

runBlock := run.Block("entry")
failedBlock := run.Block("failed")
exitBlock := run.Block("exit")

successful := runBlock.Add(&ir.CallInst{Callee: testParam})
runBlock.Add(&ir.BrInst{Condition: successful, True: exitBlock, False: failedBlock})

failedMsg := m.Constant("failed", &ir.StringConst{Length: 4, Value: []byte("%d\n\000")})

failedBlock.Add(&ir.CallInst{
Callee: printf,
Args: []ir.Value{
failedMsg,
testIndexParam,
},
})
failedBlock.Add(&ir.RetInst{})

exitBlock.Add(&ir.RetInst{})

// Main
main := m.Define("main", &ir.FuncType{Returns: ir.I32}, 0)
mainBlock := main.Block("entry")

for i, test := range tests {
mainBlock.Add(&ir.CallInst{
Callee: run,
Args: []ir.Value{
m.Declare(test.MangledName(), testType),
&ir.IntConst{Typ: ir.I32, Value: ir.Unsigned(uint64(i))},
},
})
}

mainBlock.Add(&ir.RetInst{Value: &ir.IntConst{Typ: ir.I32, Value: ir.Unsigned(0)}})

return m
}

// Test collection

func getTests(project *workspace.Project) []*ast.Func {
collector := testCollector{}

for _, file := range project.Files {
collector.VisitNode(file.Ast)
}

return collector.tests
}

type testCollector struct {
tests []*ast.Func
}

func (t *testCollector) VisitNode(node ast.Node) {
switch node := node.(type) {
case *ast.Func:
name := node.TestName()

if name != "" {
t.tests = append(t.tests, node)
}

case ast.Decl, *ast.File:
node.AcceptChildren(t)
}
}
2 changes: 1 addition & 1 deletion cmd/lsp/annotator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func annotate(node ast.Node) []protocol.InlayHint {
}

for _, decl := range node.(*ast.File).Decls {
if f, ok := decl.(*ast.Func); ok {
if f, ok := decl.(*ast.Func); ok && f.Name != nil {
a.functions[f.Name.String()] = f
}
}
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func main() {
root.AddCommand(
cmd.GetBuildCmd(),
cmd.GetRunCmd(),
cmd.GetTestCmd(),
cmd.GetInitCommand(),
lsp.GetCmd(),
)
Expand Down
14 changes: 14 additions & 0 deletions core/ast/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ func (f *Func) IntrinsicName() string {
return ""
}

func (f *Func) TestName() string {
for _, attribute := range f.Attributes {
if attribute.Name.String() == "Test" {
if len(attribute.Args) > 0 {
return attribute.Args[0].String()[1 : len(attribute.Args[0].String())-1]
}

return f.Name.String()
}
}

return ""
}

func (f *Func) HasBody() bool {
for _, attribute := range f.Attributes {
if attribute.Name.String() == "Extern" || attribute.Name.String() == "Intrinsic" {
Expand Down
13 changes: 13 additions & 0 deletions core/checker/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ func (c *checker) visitAttribute(decl *ast.Func, attribute *ast.Attribute) {
c.error(attribute.Name, "Inline doesn't have any arguments")
}

case "Test":
if len(attribute.Args) > 1 {
c.error(attribute.Name, "Test attribute can only have one argument")
}

if !ast.IsPrimitive(decl.Returns, ast.Bool) {
c.error(attribute.Name, "Tests need to return a boolean")
}

if len(decl.Params) != 0 {
c.error(attribute.Name, "Tests can't have any parameters")
}

default:
c.error(attribute.Name, "Attribute with this name doesn't exist")
}
Expand Down
15 changes: 13 additions & 2 deletions core/checker/declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,16 @@ func (c *checker) VisitFunc(decl *ast.Func) {

isExtern := false
isIntrinsic := false
isTest := false

for _, attribute := range decl.Attributes {
if attribute.Name.String() == "Extern" {
switch attribute.Name.String() {
case "Extern":
isExtern = true
} else if attribute.Name.String() == "Intrinsic" {
case "Intrinsic":
isIntrinsic = true
case "Test":
isTest = true
}
}

Expand All @@ -200,11 +204,18 @@ func (c *checker) VisitFunc(decl *ast.Func) {
if isImpl && isIntrinsic && !decl.IsStatic() {
c.error(decl.Name, "Non static methods can't be intrinsics")
}
if isImpl && isTest && !decl.IsStatic() {
c.error(decl.Name, "Non static methods can't be a test")
}

if decl.IsVariadic() && !isExtern {
c.error(decl.Name, "Only extern functions can be variadic")
}

if (isExtern && isIntrinsic) || (isExtern && isTest) || (isIntrinsic && isTest) {
c.error(decl.Name, "Invalid combination of attributes")
}

// Push scope
c.function = decl
c.pushScope()
Expand Down
7 changes: 7 additions & 0 deletions core/checker/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,13 @@ func (c *checker) VisitUnary(expr *ast.Unary) {
expr.Result().SetValue(result.Type, 0, nil)

case scanner.FuncPtr:
if result.Kind != ast.CallableResultKind {
c.error(expr.Value, "Invalid value")
expr.Result().SetInvalid()

return
}

if f, ok := result.Callable().(*ast.Func); ok && f.Method() != nil {
c.error(expr.Value, "Cannot take address of a non-static method")
}
Expand Down
2 changes: 1 addition & 1 deletion core/codegen/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ func (c *codegen) VisitLogical(expr *ast.Logical) {
startBlock := c.block

c.setLocationMeta(
c.block.Add(&ir.BrInst{Condition: left.v, True: end, False: end}),
c.block.Add(&ir.BrInst{Condition: left.v, True: true_, False: end}),
expr,
)

Expand Down
7 changes: 4 additions & 3 deletions core/codegen/statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,11 @@ func (c *codegen) VisitFor(stmt *ast.For) {
c.beginBlock(body)
}

c.acceptStmt(stmt.Body)
c.acceptExpr(stmt.Increment)
if c.acceptStmt(stmt.Body) {
c.acceptExpr(stmt.Increment)

c.block.Add(&ir.BrInst{True: c.loopStart})
c.block.Add(&ir.BrInst{True: c.loopStart})
}

// End
c.scopes.pop()
Expand Down
4 changes: 2 additions & 2 deletions core/scanner/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ const (
FuncPtr
Hashtag
DotDotDot
And
Or

Nil
True
False
And
Or
Var
If
Else
Expand Down
3 changes: 3 additions & 0 deletions tests/project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Name = "Tests"
Src = "src"
Namespace = "Tests"
26 changes: 26 additions & 0 deletions tests/src/arrays.fb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Tests.Arrays;

#[Test]
func index() bool {
var a = [ 5, 9 ];

return a[0] == 5;
}

#[Test]
func assign() bool {
var a [3]i32;
a[1] = 2;

return a[1] == 2;
}

#[Test]
func pointer() bool {
var a = [ 5, 9 ];

var ptr = &a[1];
*ptr = 12;

return a[1] == 12;
}
Loading

0 comments on commit 20a9ed1

Please sign in to comment.