// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package constraint import ( "fmt" "maps" "reflect" "slices" "strings" "testing" ) var exprStringTests = []struct { x Expr out string }{ { x: tag("abc"), out: "abc", }, { x: not(tag("abc")), out: "!abc", }, { x: not(and(tag("abc"), tag("def"))), out: "!(abc && def)", }, { x: and(tag("abc"), or(tag("def"), tag("ghi"))), out: "abc && (def || ghi)", }, { x: or(and(tag("abc"), tag("def")), tag("ghi")), out: "(abc && def) || ghi", }, } func TestExprString(t *testing.T) { for i, tt := range exprStringTests { t.Run(fmt.Sprint(i), func(t *testing.T) { s := tt.x.String() if s != tt.out { t.Errorf("String() mismatch:\nhave %s\nwant %s", s, tt.out) } }) } } var lexTests = []struct { in string out string }{ {"", ""}, {"x", "x"}, {"x.y", "x.y"}, {"x_y", "x_y"}, {"αx", "αx"}, {"αx²", "αx err: invalid syntax at ²"}, {"go1.2", "go1.2"}, {"x y", "x y"}, {"x!y", "x ! y"}, {"&&||!()xy yx ", "&& || ! ( ) xy yx"}, {"x~", "x err: invalid syntax at ~"}, {"x ~", "x err: invalid syntax at ~"}, {"x &", "x err: invalid syntax at &"}, {"x &y", "x err: invalid syntax at &"}, } func TestLex(t *testing.T) { for i, tt := range lexTests { t.Run(fmt.Sprint(i), func(t *testing.T) { p := &exprParser{s: tt.in} out := "" for { tok, err := lexHelp(p) if tok == "" && err == nil { break } if out != "" { out += " " } if err != nil { out += "err: " + err.Error() break } out += tok } if out != tt.out { t.Errorf("lex(%q):\nhave %s\nwant %s", tt.in, out, tt.out) } }) } } func lexHelp(p *exprParser) (tok string, err error) { defer func() { if e := recover(); e != nil { if e, ok := e.(*SyntaxError); ok { err = e return } panic(e) } }() p.lex() return p.tok, nil } var parseExprTests = []struct { in string x Expr }{ {"x", tag("x")}, {"x&&y", and(tag("x"), tag("y"))}, {"x||y", or(tag("x"), tag("y"))}, {"(x)", tag("x")}, {"x||y&&z", or(tag("x"), and(tag("y"), tag("z")))}, {"x&&y||z", or(and(tag("x"), tag("y")), tag("z"))}, {"x&&(y||z)", and(tag("x"), or(tag("y"), tag("z")))}, {"(x||y)&&z", and(or(tag("x"), tag("y")), tag("z"))}, {"!(x&&y)", not(and(tag("x"), tag("y")))}, } func TestParseExpr(t *testing.T) { for i, tt := range parseExprTests { t.Run(fmt.Sprint(i), func(t *testing.T) { x, err := parseExpr(tt.in) if err != nil { t.Fatal(err) } if x.String() != tt.x.String() { t.Errorf("parseExpr(%q):\nhave %s\nwant %s", tt.in, x, tt.x) } }) } } var parseExprErrorTests = []struct { in string err error }{ {"x && ", &SyntaxError{Offset: 5, Err: "unexpected end of expression"}}, {"x && (", &SyntaxError{Offset: 6, Err: "missing close paren"}}, {"x && ||", &SyntaxError{Offset: 5, Err: "unexpected token ||"}}, {"x && !", &SyntaxError{Offset: 6, Err: "unexpected end of expression"}}, {"x && !!", &SyntaxError{Offset: 6, Err: "double negation not allowed"}}, {"x !", &SyntaxError{Offset: 2, Err: "unexpected token !"}}, {"x && (y", &SyntaxError{Offset: 5, Err: "missing close paren"}}, } func TestParseError(t *testing.T) { for i, tt := range parseExprErrorTests { t.Run(fmt.Sprint(i), func(t *testing.T) { x, err := parseExpr(tt.in) if err == nil { t.Fatalf("parseExpr(%q) = %v, want error", tt.in, x) } if !reflect.DeepEqual(err, tt.err) { t.Fatalf("parseExpr(%q): wrong error:\nhave %#v\nwant %#v", tt.in, err, tt.err) } }) } } var exprEvalTests = []struct { in string ok bool tags string }{ {"x", false, "x"}, {"x && y", false, "x y"}, {"x || y", false, "x y"}, {"!x && yes", true, "x yes"}, {"yes || y", true, "y yes"}, } func TestExprEval(t *testing.T) { for i, tt := range exprEvalTests { t.Run(fmt.Sprint(i), func(t *testing.T) { x, err := parseExpr(tt.in) if err != nil { t.Fatal(err) } tags := make(map[string]bool) wantTags := make(map[string]bool) for _, tag := range strings.Fields(tt.tags) { wantTags[tag] = true } hasTag := func(tag string) bool { tags[tag] = true return tag == "yes" } ok := x.Eval(hasTag) if ok != tt.ok || !maps.Equal(tags, wantTags) { t.Errorf("Eval(%#q):\nhave ok=%v, tags=%v\nwant ok=%v, tags=%v", tt.in, ok, tags, tt.ok, wantTags) } }) } } var parsePlusBuildExprTests = []struct { in string x Expr }{ {"x", tag("x")}, {"x,y", and(tag("x"), tag("y"))}, {"x y", or(tag("x"), tag("y"))}, {"x y,z", or(tag("x"), and(tag("y"), tag("z")))}, {"x,y z", or(and(tag("x"), tag("y")), tag("z"))}, {"x,!y !z", or(and(tag("x"), not(tag("y"))), not(tag("z")))}, {"!! x", or(tag("ignore"), tag("x"))}, {"!!x", tag("ignore")}, {"!x", not(tag("x"))}, {"!", tag("ignore")}, {"", tag("ignore")}, } func TestParsePlusBuildExpr(t *testing.T) { for i, tt := range parsePlusBuildExprTests { t.Run(fmt.Sprint(i), func(t *testing.T) { x, _ := parsePlusBuildExpr(tt.in) if x.String() != tt.x.String() { t.Errorf("parsePlusBuildExpr(%q):\nhave %v\nwant %v", tt.in, x, tt.x) } }) } } var constraintTests = []struct { in string x Expr err string }{ {"//+build !", tag("ignore"), ""}, {"//+build", tag("ignore"), ""}, {"//+build x y", or(tag("x"), tag("y")), ""}, {"// +build x y \n", or(tag("x"), tag("y")), ""}, {"// +build x y \n ", nil, "not a build constraint"}, {"// +build x y \nmore", nil, "not a build constraint"}, {" //+build x y", nil, "not a build constraint"}, {"//go:build x && y", and(tag("x"), tag("y")), ""}, {"//go:build x && y\n", and(tag("x"), tag("y")), ""}, {"//go:build x && y\n ", nil, "not a build constraint"}, {"//go:build x && y\nmore", nil, "not a build constraint"}, {" //go:build x && y", nil, "not a build constraint"}, {"//go:build\n", nil, "unexpected end of expression"}, } func TestParse(t *testing.T) { for i, tt := range constraintTests { t.Run(fmt.Sprint(i), func(t *testing.T) { x, err := Parse(tt.in) if err != nil { if tt.err == "" { t.Errorf("Constraint(%q): unexpected error: %v", tt.in, err) } else if !strings.Contains(err.Error(), tt.err) { t.Errorf("Constraint(%q): error %v, want %v", tt.in, err, tt.err) } return } if tt.err != "" { t.Errorf("Constraint(%q) = %v, want error %v", tt.in, x, tt.err) return } if x.String() != tt.x.String() { t.Errorf("Constraint(%q):\nhave %v\nwant %v", tt.in, x, tt.x) } }) } } var plusBuildLinesTests = []struct { in string out []string err error }{ {"x", []string{"x"}, nil}, {"x && !y", []string{"x,!y"}, nil}, {"x || y", []string{"x y"}, nil}, {"x && (y || z)", []string{"x", "y z"}, nil}, {"!(x && y)", []string{"!x !y"}, nil}, {"x || (y && z)", []string{"x y,z"}, nil}, {"w && (x || (y && z))", []string{"w", "x y,z"}, nil}, {"v || (w && (x || (y && z)))", nil, errComplex}, } func TestPlusBuildLines(t *testing.T) { for i, tt := range plusBuildLinesTests { t.Run(fmt.Sprint(i), func(t *testing.T) { x, err := parseExpr(tt.in) if err != nil { t.Fatal(err) } lines, err := PlusBuildLines(x) if err != nil { if tt.err == nil { t.Errorf("PlusBuildLines(%q): unexpected error: %v", tt.in, err) } else if tt.err != err { t.Errorf("PlusBuildLines(%q): error %v, want %v", tt.in, err, tt.err) } return } if tt.err != nil { t.Errorf("PlusBuildLines(%q) = %v, want error %v", tt.in, lines, tt.err) return } var want []string for _, line := range tt.out { want = append(want, "// +build "+line) } if !slices.Equal(lines, want) { t.Errorf("PlusBuildLines(%q):\nhave %q\nwant %q", tt.in, lines, want) } }) } } func TestSizeLimits(t *testing.T) { for _, tc := range []struct { name string expr string }{ { name: "go:build or limit", expr: "//go:build " + strings.Repeat("a || ", maxSize+2), }, { name: "go:build and limit", expr: "//go:build " + strings.Repeat("a && ", maxSize+2), }, { name: "go:build and depth limit", expr: "//go:build " + strings.Repeat("(a &&", maxSize+2), }, { name: "go:build or depth limit", expr: "//go:build " + strings.Repeat("(a ||", maxSize+2), }, } { t.Run(tc.name, func(t *testing.T) { _, err := Parse(tc.expr) if err == nil { t.Error("expression did not trigger limit") } else if syntaxErr, ok := err.(*SyntaxError); !ok || syntaxErr.Err != "build expression too large" { if !ok { t.Errorf("unexpected error: %v", err) } else { t.Errorf("unexpected syntax error: %s", syntaxErr.Err) } } }) } } func TestPlusSizeLimits(t *testing.T) { maxOldSize := 100 for _, tc := range []struct { name string expr string }{ { name: "+build or limit", expr: "// +build " + strings.Repeat("a ", maxOldSize+2), }, { name: "+build and limit", expr: "// +build " + strings.Repeat("a,", maxOldSize+2), }, } { t.Run(tc.name, func(t *testing.T) { _, err := Parse(tc.expr) if err == nil { t.Error("expression did not trigger limit") } else if err != errComplex { t.Errorf("unexpected error: got %q, want %q", err, errComplex) } }) } }