github.com/opiuman/genqlient@v1.0.0/generate/generate_test.go (about) 1 package generate 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "testing" 11 12 "github.com/opiuman/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 := os.CreateTemp("./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 := os.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/opiuman/genqlient/internal/testutil.ID"}, 87 "DateTime": {Type: "time.Time"}, 88 "Date": { 89 Type: "time.Time", 90 Marshaler: "github.com/opiuman/genqlient/internal/testutil.MarshalDate", 91 Unmarshaler: "github.com/opiuman/genqlient/internal/testutil.UnmarshalDate", 92 }, 93 "Junk": {Type: "interface{}"}, 94 "ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"}, 95 "Pokemon": { 96 Type: "github.com/opiuman/genqlient/internal/testutil.Pokemon", 97 ExpectExactFields: "{ species level }", 98 }, 99 "PokemonInput": {Type: "github.com/opiuman/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 getDefaultConfig(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 := os.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 operations []string // overrides the default set below 153 config *Config // omits Schema and Operations, set below. 154 }{ 155 {"DefaultConfig", "", nil, getDefaultConfig(t)}, 156 {"Subpackage", "", nil, &Config{ 157 Generated: "mypkg/myfile.go", 158 }}, 159 {"SubpackageConfig", "mypkg", nil, &Config{ 160 Generated: "myfile.go", // (relative to genqlient.yaml) 161 }}, 162 {"PackageName", "", nil, &Config{ 163 Generated: "myfile.go", 164 Package: "mypkg", 165 }}, 166 {"ExportOperations", "", nil, &Config{ 167 Generated: "generated.go", 168 ExportOperations: "operations.json", 169 }}, 170 {"CustomContext", "", nil, &Config{ 171 Generated: "generated.go", 172 ContextType: "github.com/opiuman/genqlient/internal/testutil.MyContext", 173 }}, 174 {"StructReferences", "", nil, &Config{ 175 StructReferences: true, 176 Generated: "generated-structrefs.go", 177 }}, 178 {"NoContext", "", nil, &Config{ 179 Generated: "generated.go", 180 ContextType: "-", 181 }}, 182 {"ClientGetter", "", nil, &Config{ 183 Generated: "generated.go", 184 ClientGetter: "github.com/opiuman/genqlient/internal/testutil.GetClientFromContext", 185 }}, 186 {"ClientGetterCustomContext", "", nil, &Config{ 187 Generated: "generated.go", 188 ClientGetter: "github.com/opiuman/genqlient/internal/testutil.GetClientFromMyContext", 189 ContextType: "github.com/opiuman/genqlient/internal/testutil.MyContext", 190 }}, 191 {"ClientGetterNoContext", "", nil, &Config{ 192 Generated: "generated.go", 193 ClientGetter: "github.com/opiuman/genqlient/internal/testutil.GetClientFromNowhere", 194 ContextType: "-", 195 }}, 196 {"Extensions", "", nil, &Config{ 197 Generated: "generated.go", 198 Extensions: true, 199 }}, 200 {"OptionalValue", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ 201 Generated: "generated.go", 202 Optional: "value", 203 }}, 204 {"OptionalPointer", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ 205 Generated: "generated.go", 206 Optional: "pointer", 207 }}, 208 } 209 210 sourceFilename := "SimpleQuery.graphql" 211 212 for _, test := range tests { 213 config := test.config 214 baseDir := filepath.Join(dataDir, test.baseDir) 215 t.Run(test.name, func(t *testing.T) { 216 err := config.ValidateAndFillDefaults(baseDir) 217 config.Schema = []string{filepath.Join(dataDir, "schema.graphql")} 218 if test.operations == nil { 219 config.Operations = []string{filepath.Join(dataDir, sourceFilename)} 220 } else { 221 config.Operations = make([]string, len(test.operations)) 222 for i := range test.operations { 223 config.Operations[i] = filepath.Join(dataDir, test.operations[i]) 224 } 225 } 226 if err != nil { 227 t.Fatal(err) 228 } 229 generated, err := Generate(config) 230 if err != nil { 231 t.Fatal(err) 232 } 233 234 for filename, content := range generated { 235 t.Run(filename, func(t *testing.T) { 236 testutil.Cupaloy.SnapshotT(t, string(content)) 237 }) 238 } 239 240 t.Run("Build", func(t *testing.T) { 241 if testing.Short() { 242 t.Skip("skipping build due to -short") 243 } 244 245 err := buildGoFile(sourceFilename, 246 generated[config.Generated]) 247 if err != nil { 248 t.Error(err) 249 } 250 }) 251 }) 252 } 253 } 254 255 // TestGenerateErrors is a snapshot-based test of error text. 256 // 257 // For each .go or .graphql file in testdata/errors, it asserts that the given 258 // query returns an error, and that that error's string-text matches the 259 // snapshot. The snapshotting is useful to ensure we don't accidentally make 260 // the text less readable, drop the line numbers, etc. We include both .go and 261 // .graphql tests for some of the test cases, to make sure the line numbers 262 // work in both cases. Tests may include a .schema.graphql file of their own, 263 // or use the shared schema.graphql in the same directory for convenience. 264 func TestGenerateErrors(t *testing.T) { 265 files, err := os.ReadDir(errorsDir) 266 if err != nil { 267 t.Fatal(err) 268 } 269 270 for _, file := range files { 271 sourceFilename := file.Name() 272 if !strings.HasSuffix(sourceFilename, ".graphql") && 273 !strings.HasSuffix(sourceFilename, ".go") || 274 strings.HasSuffix(sourceFilename, ".schema.graphql") || 275 sourceFilename == "schema.graphql" { 276 continue 277 } 278 279 baseFilename := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename)) 280 testFilename := strings.ReplaceAll(sourceFilename, ".", "/") 281 282 // Schema is either <base>.schema.graphql, or <dir>/schema.graphql if 283 // that doesn't exist. 284 schemaFilename := baseFilename + ".schema.graphql" 285 if _, err := os.Stat(filepath.Join(errorsDir, schemaFilename)); err != nil { 286 if errors.Is(err, os.ErrNotExist) { 287 schemaFilename = "schema.graphql" 288 } else { 289 t.Fatal(err) 290 } 291 } 292 293 t.Run(testFilename, func(t *testing.T) { 294 _, err := Generate(&Config{ 295 Schema: []string{filepath.Join(errorsDir, schemaFilename)}, 296 Operations: []string{filepath.Join(errorsDir, sourceFilename)}, 297 Package: "test", 298 Generated: os.DevNull, 299 ContextType: "context.Context", 300 Bindings: map[string]*TypeBinding{ 301 "ValidScalar": {Type: "string"}, 302 "InvalidScalar": {Type: "bogus"}, 303 "Pokemon": { 304 Type: "github.com/opiuman/genqlient/internal/testutil.Pokemon", 305 ExpectExactFields: "{ species level }", 306 }, 307 }, 308 AllowBrokenFeatures: true, 309 }) 310 if err == nil { 311 t.Fatal("expected an error") 312 } 313 314 testutil.Cupaloy.SnapshotT(t, err.Error()) 315 }) 316 } 317 }