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