github.com/nathanstitt/genqlient@v0.3.1-0.20211028004951-a2bda3c41ab8/generate/generate_test.go (about) 1 package generate 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "testing" 11 12 "github.com/Khan/genqlient/internal/testutil" 13 "gopkg.in/yaml.v2" 14 ) 15 16 const ( 17 dataDir = "testdata/queries" 18 errorsDir = "testdata/errors" 19 ) 20 21 // buildGoFile returns an error if the given Go code is not valid. 22 // 23 // namePrefix is used for the temp-file, and is just for debugging. 24 func buildGoFile(namePrefix string, content []byte) error { 25 // We need to put this within the current module, rather than in 26 // /tmp, so that it can access internal/testutil. 27 f, err := ioutil.TempFile("./testdata/tmp", namePrefix+"_*.go") 28 if err != nil { 29 return err 30 } 31 defer func() { 32 f.Close() 33 os.Remove(f.Name()) 34 }() 35 36 _, err = f.Write(content) 37 if err != nil { 38 return err 39 } 40 41 cmd := exec.Command("go", "build", f.Name()) 42 cmd.Stdout = os.Stdout 43 cmd.Stderr = os.Stderr 44 err = cmd.Run() 45 if err != nil { 46 return fmt.Errorf("generated code does not compile: %w", err) 47 } 48 return nil 49 } 50 51 // TestGenerate is a snapshot-based test of code-generation. 52 // 53 // This file just has the test runner; the actual data is all in 54 // testdata/queries. Specifically, the schema used for all the queries is in 55 // schema.graphql; the queries themselves are in TestName.graphql. The test 56 // asserts that running genqlient on that query produces the generated code in 57 // the snapshot-file TestName.graphql.go. 58 // 59 // To update the snapshots (if the code-generator has changed), run the test 60 // with `UPDATE_SNAPSHOTS=1`; it will fail the tests and print any diffs, but 61 // update the snapshots. Make sure to check that the output is sensible; the 62 // snapshots don't even get compiled! 63 func TestGenerate(t *testing.T) { 64 files, err := ioutil.ReadDir(dataDir) 65 if err != nil { 66 t.Fatal(err) 67 } 68 69 for _, file := range files { 70 sourceFilename := file.Name() 71 if sourceFilename == "schema.graphql" || !strings.HasSuffix(sourceFilename, ".graphql") { 72 continue 73 } 74 goFilename := sourceFilename + ".go" 75 queriesFilename := sourceFilename + ".json" 76 77 t.Run(sourceFilename, func(t *testing.T) { 78 generated, err := Generate(&Config{ 79 Schema: []string{filepath.Join(dataDir, "schema.graphql")}, 80 Operations: []string{filepath.Join(dataDir, sourceFilename)}, 81 Package: "test", 82 Generated: goFilename, 83 ExportOperations: queriesFilename, 84 ContextType: "-", 85 Bindings: map[string]*TypeBinding{ 86 "ID": {Type: "github.com/Khan/genqlient/internal/testutil.ID"}, 87 "DateTime": {Type: "time.Time"}, 88 "Date": { 89 Type: "time.Time", 90 Marshaler: "github.com/Khan/genqlient/internal/testutil.MarshalDate", 91 Unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate", 92 }, 93 "Junk": {Type: "interface{}"}, 94 "ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"}, 95 "Pokemon": { 96 Type: "github.com/Khan/genqlient/internal/testutil.Pokemon", 97 ExpectExactFields: "{ species level }", 98 }, 99 "PokemonInput": {Type: "github.com/Khan/genqlient/internal/testutil.Pokemon"}, 100 }, 101 AllowBrokenFeatures: true, 102 }) 103 if err != nil { 104 t.Fatal(err) 105 } 106 107 for filename, content := range generated { 108 t.Run(filename, func(t *testing.T) { 109 testutil.Cupaloy.SnapshotT(t, string(content)) 110 }) 111 } 112 113 t.Run("Build", func(t *testing.T) { 114 if testing.Short() { 115 t.Skip("skipping build due to -short") 116 } 117 118 err := buildGoFile(sourceFilename, generated[goFilename]) 119 if err != nil { 120 t.Error(err) 121 } 122 }) 123 }) 124 } 125 } 126 127 func defaultConfig(t *testing.T) *Config { 128 // Parse the config that `genqlient --init` generates, to make sure that 129 // works. 130 var config Config 131 b, err := ioutil.ReadFile("default_genqlient.yaml") 132 if err != nil { 133 t.Fatal(err) 134 } 135 136 err = yaml.UnmarshalStrict(b, &config) 137 if err != nil { 138 t.Fatal(err) 139 } 140 return &config 141 } 142 143 // TestGenerateWithConfig tests several configuration options that affect 144 // generated code but don't require particular query structures to test. 145 // 146 // It runs a simple query from TestGenerate with several different genqlient 147 // configurations. It uses snapshots, just like TestGenerate. 148 func TestGenerateWithConfig(t *testing.T) { 149 tests := []struct { 150 name string 151 baseDir string // relative to dataDir 152 config *Config // omits Schema and Operations, set below. 153 }{ 154 {"DefaultConfig", "", defaultConfig(t)}, 155 {"Subpackage", "", &Config{ 156 Generated: "mypkg/myfile.go", 157 }}, 158 {"SubpackageConfig", "mypkg", &Config{ 159 Generated: "myfile.go", // (relative to genqlient.yaml) 160 }}, 161 {"PackageName", "", &Config{ 162 Generated: "myfile.go", 163 Package: "mypkg", 164 }}, 165 {"ExportOperations", "", &Config{ 166 Generated: "generated.go", 167 ExportOperations: "operations.json", 168 }}, 169 {"CustomContext", "", &Config{ 170 Generated: "generated.go", 171 ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext", 172 }}, 173 {"NoContext", "", &Config{ 174 Generated: "generated.go", 175 ContextType: "-", 176 }}, 177 {"ClientGetter", "", &Config{ 178 Generated: "generated.go", 179 ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromContext", 180 }}, 181 {"ClientGetterCustomContext", "", &Config{ 182 Generated: "generated.go", 183 ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromMyContext", 184 ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext", 185 }}, 186 {"ClientGetterNoContext", "", &Config{ 187 Generated: "generated.go", 188 ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromNowhere", 189 ContextType: "-", 190 }}, 191 } 192 193 sourceFilename := "SimpleQuery.graphql" 194 195 for _, test := range tests { 196 config := test.config 197 baseDir := filepath.Join(dataDir, test.baseDir) 198 t.Run(test.name, func(t *testing.T) { 199 err := config.ValidateAndFillDefaults(baseDir) 200 config.Schema = []string{filepath.Join(dataDir, "schema.graphql")} 201 config.Operations = []string{filepath.Join(dataDir, sourceFilename)} 202 if err != nil { 203 t.Fatal(err) 204 } 205 generated, err := Generate(config) 206 if err != nil { 207 t.Fatal(err) 208 } 209 210 for filename, content := range generated { 211 t.Run(filename, func(t *testing.T) { 212 testutil.Cupaloy.SnapshotT(t, string(content)) 213 }) 214 } 215 216 t.Run("Build", func(t *testing.T) { 217 if testing.Short() { 218 t.Skip("skipping build due to -short") 219 } 220 221 err := buildGoFile(sourceFilename, 222 generated[config.Generated]) 223 if err != nil { 224 t.Error(err) 225 } 226 }) 227 }) 228 } 229 } 230 231 // TestGenerate is a snapshot-based test of error text. 232 // 233 // For each .go or .graphql file in testdata/errors, and corresponding 234 // .schema.graphql file, it asserts that the given query returns an error, and 235 // that that error's string-text matches the snapshot. The snapshotting is 236 // useful to ensure we don't accidentally make the text less readable, drop the 237 // line numbers, etc. We include both .go and .graphql tests, to make sure the 238 // line numbers work in both cases. 239 func TestGenerateErrors(t *testing.T) { 240 files, err := ioutil.ReadDir(errorsDir) 241 if err != nil { 242 t.Fatal(err) 243 } 244 245 for _, file := range files { 246 sourceFilename := file.Name() 247 if !strings.HasSuffix(sourceFilename, ".graphql") && 248 !strings.HasSuffix(sourceFilename, ".go") || 249 strings.HasSuffix(sourceFilename, ".schema.graphql") { 250 continue 251 } 252 253 baseFilename := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename)) 254 schemaFilename := baseFilename + ".schema.graphql" 255 testFilename := strings.ReplaceAll(sourceFilename, ".", "/") 256 257 t.Run(testFilename, func(t *testing.T) { 258 _, err := Generate(&Config{ 259 Schema: []string{filepath.Join(errorsDir, schemaFilename)}, 260 Operations: []string{filepath.Join(errorsDir, sourceFilename)}, 261 Package: "test", 262 Generated: os.DevNull, 263 ContextType: "context.Context", 264 Bindings: map[string]*TypeBinding{ 265 "ValidScalar": {Type: "string"}, 266 "InvalidScalar": {Type: "bogus"}, 267 "Pokemon": { 268 Type: "github.com/Khan/genqlient/internal/testutil.Pokemon", 269 ExpectExactFields: "{ species level }", 270 }, 271 }, 272 AllowBrokenFeatures: true, 273 }) 274 if err == nil { 275 t.Fatal("expected an error") 276 } 277 278 testutil.Cupaloy.SnapshotT(t, err.Error()) 279 }) 280 } 281 }