Skip to content

Commit

Permalink
Split tests for instruction and model parsing
Browse files Browse the repository at this point in the history
The goal of this commit is to prepare the code to have more modular tests for instruction parsing. Precisely, instruction parsing will be black-box tested similarly to how  ParseModel is tested.
  • Loading branch information
rhartert committed May 26, 2024
1 parent 31c878a commit 3ce3643
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 146 deletions.
5 changes: 5 additions & 0 deletions fzn/fzn.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ func Parse(reader io.Reader, handler Handler) error {
i := 0 // line number
for scanner.Scan() {
i++

line := scanner.Text()
if line == "" { // TODO: this should ideally be done in the parser itself
continue
}

tokens, err := tokenizer.Tokenize(line)
if err != nil {
return fmt.Errorf("tokenizer error at line %d: %w", i, err)
Expand Down
159 changes: 159 additions & 0 deletions fzn/fzn_model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package fzn

import (
_ "embed"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/rhartert/ptr"
)

type testCaseModel struct {
desc string
input string
want *Model
wantErr bool
}

var testCakesFZN = `
array [1..2] of int: X_INTRODUCED_2_ = [250,200];
array [1..2] of int: X_INTRODUCED_6_ = [75,150];
array [1..2] of int: X_INTRODUCED_8_ = [100,150];
var 0..3: b:: output_var;
var 0..6: c:: output_var;
var 0..85000: X_INTRODUCED_0_:: is_defined_var;
constraint int_lin_le(X_INTRODUCED_2_,[b,c],4000);
constraint int_lin_le(X_INTRODUCED_6_,[b,c],2000);
constraint int_lin_le(X_INTRODUCED_8_,[b,c],500);
constraint int_lin_eq([400,450,-1],[b,c,X_INTRODUCED_0_],0):: ctx_pos:: defines_var(X_INTRODUCED_0_);
solve maximize X_INTRODUCED_0_;
`

var testCases = []testCaseModel{
{
desc: "cake.fzn",
input: testCakesFZN,
want: &Model{
Parameters: []Parameter{
{
Identifier: "X_INTRODUCED_2_",
Array: &Array{Start: 1, End: 2},
Type: ParTypeInt,
Exprs: []BasicLitExpr{
{Int: ptr.Of(250)},
{Int: ptr.Of(200)},
},
},
{
Identifier: "X_INTRODUCED_6_",
Array: &Array{Start: 1, End: 2},
Type: ParTypeInt,
Exprs: []BasicLitExpr{
{Int: ptr.Of(75)},
{Int: ptr.Of(150)},
},
},
{
Identifier: "X_INTRODUCED_8_",
Array: &Array{Start: 1, End: 2},
Type: ParTypeInt,
Exprs: []BasicLitExpr{
{Int: ptr.Of(100)},
{Int: ptr.Of(150)},
},
},
},
Variables: []Variable{
{
Identifier: "b",
Type: VarTypeIntRange,
Domain: VarDomain{IntDomain: &SetIntLit{Values: [][]int{{0, 3}}}},
Annotations: []Annotation{{Identifier: "output_var"}},
},
{
Identifier: "c",
Type: VarTypeIntRange,
Domain: VarDomain{IntDomain: &SetIntLit{Values: [][]int{{0, 6}}}},
Annotations: []Annotation{{Identifier: "output_var"}},
},
{
Identifier: "X_INTRODUCED_0_",
Type: VarTypeIntRange,
Domain: VarDomain{IntDomain: &SetIntLit{Values: [][]int{{0, 85000}}}},
Annotations: []Annotation{{Identifier: "is_defined_var"}},
},
},
Constraints: []Constraint{
{
Identifier: "int_lin_le",
Expressions: []Expr{
{Exprs: []BasicExpr{{Identifier: "X_INTRODUCED_2_"}}},
{IsArray: true, Exprs: []BasicExpr{{Identifier: "b"}, {Identifier: "c"}}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(4000)}}}},
},
},
{
Identifier: "int_lin_le",
Expressions: []Expr{
{Exprs: []BasicExpr{{Identifier: "X_INTRODUCED_6_"}}},
{IsArray: true, Exprs: []BasicExpr{{Identifier: "b"}, {Identifier: "c"}}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(2000)}}}},
},
},
{
Identifier: "int_lin_le",
Expressions: []Expr{
{Exprs: []BasicExpr{{Identifier: "X_INTRODUCED_8_"}}},
{IsArray: true, Exprs: []BasicExpr{{Identifier: "b"}, {Identifier: "c"}}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(500)}}}},
},
},
{
Identifier: "int_lin_eq",
Expressions: []Expr{
{IsArray: true, Exprs: []BasicExpr{
{LiteralExpr: BasicLitExpr{Int: ptr.Of(400)}},
{LiteralExpr: BasicLitExpr{Int: ptr.Of(450)}},
{LiteralExpr: BasicLitExpr{Int: ptr.Of(-1)}},
}},
{IsArray: true, Exprs: []BasicExpr{
{Identifier: "b"},
{Identifier: "c"},
{Identifier: "X_INTRODUCED_0_"},
}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(0)}}}},
},
Annotations: []Annotation{
{Identifier: "ctx_pos"},
{Identifier: "defines_var", Exprs: [][]AnnExpr{
{{VarID: ptr.Of("X_INTRODUCED_0_")}},
}},
},
},
},
SolveGoals: []SolveGoal{{
SolveMethod: SolveMethodMaximize,
Objective: BasicExpr{Identifier: "X_INTRODUCED_0_"},
}},
},
},
}

func TestParseModel(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
got, gotErr := ParseModel(strings.NewReader(tc.input))

if tc.wantErr && gotErr == nil {
t.Errorf("ParseModel(): want error, got nil")
}
if !tc.wantErr && gotErr != nil {
t.Errorf("ParseModel(): want no error, got %s", gotErr)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("ParseModel(): mismatch (-want +got):\n%s", diff)
}
})
}
}
167 changes: 32 additions & 135 deletions fzn/fzn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,163 +3,60 @@ package fzn
import (
_ "embed"
"errors"
"io"
"strings"
"testing"
"testing/iotest"

"github.com/google/go-cmp/cmp"
"github.com/rhartert/ptr"
)

//go:embed testdata/cakes.fzn
var testCakesFZN string

type testCase struct {
desc string
input io.Reader
want *Model
type testCaseInstruction struct {
input string
want instruction
wantErr bool
}

var testCases = []testCase{
{
desc: "error reader",
input: iotest.ErrReader(errors.New("test error")),
wantErr: true,
},
{
desc: "invalid input (tokenizer)",
input: strings.NewReader("@@foo-bar%**!!"),
wantErr: true,
},
{
desc: "incorrect input (parser)",
input: strings.NewReader("var [[ array ))"),
wantErr: true,
},
{
desc: "cake.fzn",
input: strings.NewReader(testCakesFZN),
want: &Model{
Parameters: []Parameter{
{
Identifier: "X_INTRODUCED_2_",
Array: &Array{Start: 1, End: 2},
Type: ParTypeInt,
Exprs: []BasicLitExpr{
{Int: ptr.Of(250)},
{Int: ptr.Of(200)},
},
},
{
Identifier: "X_INTRODUCED_6_",
Array: &Array{Start: 1, End: 2},
Type: ParTypeInt,
Exprs: []BasicLitExpr{
{Int: ptr.Of(75)},
{Int: ptr.Of(150)},
},
},
{
Identifier: "X_INTRODUCED_8_",
Array: &Array{Start: 1, End: 2},
Type: ParTypeInt,
Exprs: []BasicLitExpr{
{Int: ptr.Of(100)},
{Int: ptr.Of(150)},
},
},
},
Variables: []Variable{
{
Identifier: "b",
Type: VarTypeIntRange,
Domain: VarDomain{IntDomain: &SetIntLit{Values: [][]int{{0, 3}}}},
Annotations: []Annotation{{Identifier: "output_var"}},
},
{
Identifier: "c",
Type: VarTypeIntRange,
Domain: VarDomain{IntDomain: &SetIntLit{Values: [][]int{{0, 6}}}},
Annotations: []Annotation{{Identifier: "output_var"}},
},
{
Identifier: "X_INTRODUCED_0_",
Type: VarTypeIntRange,
Domain: VarDomain{IntDomain: &SetIntLit{Values: [][]int{{0, 85000}}}},
Annotations: []Annotation{{Identifier: "is_defined_var"}},
},
},
Constraints: []Constraint{
{
Identifier: "int_lin_le",
Expressions: []Expr{
{Exprs: []BasicExpr{{Identifier: "X_INTRODUCED_2_"}}},
{IsArray: true, Exprs: []BasicExpr{{Identifier: "b"}, {Identifier: "c"}}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(4000)}}}},
},
},
{
Identifier: "int_lin_le",
Expressions: []Expr{
{Exprs: []BasicExpr{{Identifier: "X_INTRODUCED_6_"}}},
{IsArray: true, Exprs: []BasicExpr{{Identifier: "b"}, {Identifier: "c"}}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(2000)}}}},
},
},
{
Identifier: "int_lin_le",
Expressions: []Expr{
{Exprs: []BasicExpr{{Identifier: "X_INTRODUCED_8_"}}},
{IsArray: true, Exprs: []BasicExpr{{Identifier: "b"}, {Identifier: "c"}}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(500)}}}},
},
},
{
Identifier: "int_lin_eq",
Expressions: []Expr{
{IsArray: true, Exprs: []BasicExpr{
{LiteralExpr: BasicLitExpr{Int: ptr.Of(400)}},
{LiteralExpr: BasicLitExpr{Int: ptr.Of(450)}},
{LiteralExpr: BasicLitExpr{Int: ptr.Of(-1)}},
}},
{IsArray: true, Exprs: []BasicExpr{
{Identifier: "b"},
{Identifier: "c"},
{Identifier: "X_INTRODUCED_0_"},
}},
{Exprs: []BasicExpr{{LiteralExpr: BasicLitExpr{Int: ptr.Of(0)}}}},
},
Annotations: []Annotation{
{Identifier: "ctx_pos"},
{Identifier: "defines_var", Exprs: [][]AnnExpr{
{{VarID: ptr.Of("X_INTRODUCED_0_")}},
}},
},
func TestParse_invalidReader(t *testing.T) {
got := instruction{}
gotErr := Parse(iotest.ErrReader(errors.New("test error")), &got)

if gotErr == nil {
t.Errorf("Parse(): want error, got none")
}
if diff := cmp.Diff(instruction{}, got); diff != "" {
t.Errorf("Parse(): mismatch (-want +got):\n%s", diff)
}
}

func TestParse_error(t *testing.T) {
testParse(t, []testCaseInstruction{
{
input: "var int: X;",
want: instruction{
Variable: &Variable{
Identifier: "X",
Type: VarTypeIntRange,
Domain: VarDomain{},
},
},
SolveGoals: []SolveGoal{{
SolveMethod: SolveMethodMaximize,
Objective: BasicExpr{Identifier: "X_INTRODUCED_0_"},
}},
},
},
})
}

func TestParseModel(t *testing.T) {
func testParse(t *testing.T, testCases []testCaseInstruction) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
got, gotErr := ParseModel(tc.input)
t.Run(tc.input, func(t *testing.T) {
got := instruction{}
gotErr := Parse(strings.NewReader(tc.input), &got)

if tc.wantErr && gotErr == nil {
t.Errorf("ParseModel(): want error, got nil")
t.Errorf("Parse(): want error, got nil")
}
if !tc.wantErr && gotErr != nil {
t.Errorf("ParseModel(): want no error, got %s", gotErr)
t.Errorf("Parse(): want no error, got %s", gotErr)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("ParseModel(): mismatch (-want +got):\n%s", diff)
t.Errorf("Parse(): mismatch (-want +got):\n%s", diff)
}
})
}
Expand Down
11 changes: 0 additions & 11 deletions fzn/testdata/cakes.fzn

This file was deleted.

0 comments on commit 3ce3643

Please sign in to comment.