Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ make lint # golangci-lint, config in .golangci.yml — must be cle
make fmt # gofumpt -w .
make cover # coverage.out + per-function summary
make regress REF=master # generated output, working tree vs a git ref
make e2e # generated clients against a mock and FastAPI (testdata/e2e)
go test ./internal/parser -run TestName
```

Expand All @@ -33,6 +34,7 @@ go.yaml.in/yaml/v3. No cgo.
| `internal/codegen` | the `Codegen*` structs templates render; fields mirror the Java originals |
| `internal/gen` | the pipeline: load spec → parse → render models, apis, supporting files |
| `internal/generator` | `CodegenConfig` and the language configs, `typescript/`, `dart/`, `golang/` |
| `internal/e2e` | end-to-end tests: mock server, runners, scenarios in `testdata/e2e` |
| `internal/template` | the Mustache engine: partials, lambdas, struct-to-map via json |
| `internal/config` | generator options and their config-file keys |
| `internal/update` | release check, verified download, self-update |
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help test cover cover-html lint fmt vet build install clean tidy update samples-dart-fetch samples-go-fetch regress all
.PHONY: help test cover cover-html lint fmt vet build install clean tidy update samples-dart-fetch samples-go-fetch regress e2e e2e-update all

GOFUMPT ?= go run mvdan.cc/gofumpt@v0.12.0
GOLANGCI ?= go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.0
Expand All @@ -19,6 +19,14 @@ test:
go vet ./...
go test -race ./...

## e2e: generated clients against a validating mock and the FastAPI backend (needs uv), testdata/e2e
e2e:
E2E=1 go test -count=1 -v ./internal/e2e

## e2e-update: e2e, rewriting the snapshot of the backend's spec
e2e-update:
E2E=1 E2E_UPDATE=1 go test -count=1 -v ./internal/e2e

## cover: Run tests with coverage
cover:
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3U
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
178 changes: 178 additions & 0 deletions internal/e2e/dart_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package e2e

import (
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"

"github.com/xseman/openapi-generator/internal/gen"
)

// dartVersion reads "Dart SDK version: 3.13.7 ..." as 3 and 13.
var dartVersion = regexp.MustCompile(`version: (\d+)\.(\d+)`)

// runDart generates a dart-fetch package per suite, depends on them from a
// package holding testdata/e2e/runners/dart and runs the suites on the VM.
// E2E_DART names the dart binary; the clients need Dart 3.13 or later.
func runDart(t *testing.T, suites []suite) map[string]map[int]outcome {
t.Helper()

dart := os.Getenv("E2E_DART")
if dart == "" {
dart = "dart"
}

ver, err := exec.Command(dart, "--version").CombinedOutput()
if m := dartVersion.FindStringSubmatch(string(ver)); err != nil || m == nil || m[1] == "3" && atoi(m[2]) < 13 {
t.Skipf("dart-fetch clients need Dart 3.13 or later (set E2E_DART): %s %v", strings.TrimSpace(string(ver)), err)
}

dir := t.TempDir()

runner, err := os.ReadFile("../../testdata/e2e/runners/dart/runner.dart")
if err != nil {
t.Fatal(err)
}

write(t, filepath.Join(dir, "runner.dart"), string(runner))

for i, s := range suites {
err := gen.Generate(gen.Options{
InputSpec: s.Spec,
OutputDir: filepath.Join(dir, "c"+strconv.Itoa(i)),
GeneratorName: "dart-fetch",
TemplateDir: "../../templates/dart-fetch",
AdditionalProperties: []string{"pubName=c" + strconv.Itoa(i)},
SkipValidation: true,
})
if err != nil {
t.Fatalf("generate %s: %v", s.Name, err)
}
}

// A client that does not compile would stop the whole program: it
// leaves the run, and its cases fail with its first error.
broken := dartErrors(t, dart, dir)
failed := map[string]map[int]outcome{}

var imports, clients, deps strings.Builder

for i, s := range suites {
pkg := "c" + strconv.Itoa(i)

if msg, ok := broken[pkg]; ok {
failed[s.Name] = map[int]outcome{}
for j := range s.Cases {
failed[s.Name][j] = outcome{Suite: s.Name, Case: j, Fail: "the client does not compile: " + msg}
}

continue
}

fmt.Fprintf(&deps, " %s:\n path: %s\n", pkg, pkg)
fmt.Fprintf(&imports, "import 'package:%s/%s.dart' as %s;\n", pkg, pkg, pkg)
fmt.Fprintf(&clients, " %q: Client(\n (base, a) => %s.Configuration(basePath: base, username: a.username, password: a.password, "+
"apiKey: (name) => a.apiKey ?? '', accessToken: () => a.bearer ?? ''),\n (cfg) => [%s],\n %q),\n",
s.Name, pkg, dartAPIs(t, filepath.Join(dir, pkg), pkg), pkg)
}

var running []suite

for _, s := range suites {
if _, ok := failed[s.Name]; !ok {
running = append(running, s)
}
}

if len(running) == 0 {
return failed
}

write(t, filepath.Join(dir, "pubspec.yaml"), "name: e2e_runner\npublish_to: none\nenvironment:\n sdk: '>=3.13.0 <4.0.0'\ndependencies:\n"+deps.String())
write(t, filepath.Join(dir, "main.dart"), "import 'runner.dart';\n"+imports.String()+
"\nvoid main(List<String> args) => run(args[0], {\n"+clients.String()+"});\n")

get := exec.Command(dart, "pub", "get", "--offline")
get.Dir = dir

if out, err := get.CombinedOutput(); err != nil {
t.Fatalf("dart pub get: %v\n%s", err, out)
}

writeInput(t, dir, running)

cmd := exec.Command(dart, "run", "main.dart", filepath.Join(dir, "input.json"))
cmd.Dir = dir

out := execRunner(t, cmd)
maps.Copy(out, failed)

return out
}

// dartErrors analyzes the generated packages under dir and returns the
// first compile error of each package that has one.
func dartErrors(t *testing.T, dart, dir string) map[string]string {
t.Helper()

cmd := exec.Command(dart, "analyze", "--format=machine", ".")
cmd.Dir = dir

out, _ := cmd.CombinedOutput()

errs := map[string]string{}

for _, line := range strings.Split(string(out), "\n") {
f := strings.Split(line, "|")
if len(f) < 8 || f[0] != "ERROR" {
continue
}

rel, err := filepath.Rel(dir, f[3])
if err != nil {
continue
}

pkg, _, _ := strings.Cut(rel, string(filepath.Separator))
if _, seen := errs[pkg]; !seen {
errs[pkg] = fmt.Sprintf("%s:%s: %s", rel, f[4], f[7])
}
}

return errs
}

// dartAPIs lists the constructors of a package's API classes for cfg.
func dartAPIs(t *testing.T, dir, pkg string) string {
t.Helper()

files, _ := filepath.Glob(filepath.Join(dir, "lib", "src", "apis", "*.dart"))
class := regexp.MustCompile(`(?m)^class (\w+) extends BaseApi`)

var calls []string

for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}

for _, m := range class.FindAllStringSubmatch(string(data), -1) {
calls = append(calls, pkg+"."+m[1]+"(cfg as "+pkg+".Configuration)")
}
}

return strings.Join(calls, ", ")
}

func atoi(s string) int {
n, _ := strconv.Atoi(s)

return n
}
Loading
Loading