Skip to content

Commit 5e47dc7

Browse files
committed
Merge remote-tracking branch 'upstream/main' into query-v2
# Conflicts: # .gitignore # README.md # cmd/benchmark/README.md # cmd/benchmark/main.go # cmd/benchmark/report_test.go # cmd/benchmark/runner.go
2 parents f847ea6 + 25407d6 commit 5e47dc7

39 files changed

Lines changed: 4874 additions & 128 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ integration/testdata/local/
99

1010
# Local benchmark comparison output
1111
.bench/
12+
13+
# Local test and metric artifacts
14+
.coverage/

Makefile

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,51 @@ BENCH_KIND ?= all
1212

1313
# Main packages to test/build
1414
MAIN_PACKAGES := $(shell $(GO_CMD) list ./...)
15+
COVERPKG := $(shell $(GO_CMD) list ./... | grep -v '/cypher/parser$$' | tr '\n' ',' | sed 's/,$$//')
16+
17+
# Metric configuration
18+
METRICS_DIR ?= .coverage
19+
COVERAGE_PROFILE ?= $(METRICS_DIR)/unit.out
20+
COVERAGE_FUNC_REPORT ?= $(METRICS_DIR)/coverage.txt
21+
CYCLO_REPORT ?= $(METRICS_DIR)/cyclomatic.txt
22+
CRAP_TEXT_REPORT ?= $(METRICS_DIR)/crap.txt
23+
CRAP_JSON_REPORT ?= $(METRICS_DIR)/crap.json
24+
QUALITY_TEXT_REPORT ?= $(METRICS_DIR)/quality.txt
25+
QUALITY_JSON_REPORT ?= $(METRICS_DIR)/quality.json
26+
METRICS_HTML_REPORT ?= $(METRICS_DIR)/metrics.html
27+
METRICS_IGNORE ?= (^|/)(testdata|vendor)/|_test\.go$$|^cypher/parser/
28+
CYCLO_TOP ?= 20
29+
CYCLO_OVER ?= 25
30+
CRAP_TOP ?= 20
31+
CRAP_OVER ?= 30
32+
METRICS_ENFORCE ?= 0
33+
BENCHMARK_REPORT ?=
34+
BENCHMARK_BASELINE ?=
35+
BENCHMARK_REGRESSION ?= 0.20
36+
FUZZ_REPORT ?=
37+
MUTATION_REPORT ?=
38+
BACKEND_RESULT_ARGS ?=
39+
BACKEND_PG_REPORT ?= $(METRICS_DIR)/integration-pg.json
40+
BACKEND_NEO4J_REPORT ?= $(METRICS_DIR)/integration-neo4j.json
41+
QUALITY_BENCHMARK_REPORT ?= $(METRICS_DIR)/benchmark.json
42+
QUALITY_BENCHMARK_MARKDOWN ?= $(METRICS_DIR)/benchmark.md
43+
44+
QUALITY_INPUTS := $(BACKEND_RESULT_ARGS)
45+
ifneq ($(strip $(BENCHMARK_REPORT)),)
46+
QUALITY_INPUTS += -benchmark-report $(BENCHMARK_REPORT)
47+
endif
48+
ifneq ($(strip $(BENCHMARK_BASELINE)),)
49+
QUALITY_INPUTS += -benchmark-baseline $(BENCHMARK_BASELINE)
50+
endif
51+
ifneq ($(strip $(FUZZ_REPORT)),)
52+
QUALITY_INPUTS += -fuzz-report $(FUZZ_REPORT)
53+
endif
54+
ifneq ($(strip $(MUTATION_REPORT)),)
55+
QUALITY_INPUTS += -mutation-report $(MUTATION_REPORT)
56+
endif
57+
QUALITY_INPUTS += -benchmark-regression $(BENCHMARK_REGRESSION)
58+
59+
.PHONY: default all build deps tidy lint format test test_all test_integration test_neo4j test_pg test_update complexity complexity_check crap crap_check quality quality_check quality_backend quality_bench metrics metrics_check generate clean help
1560

1661
# Default target
1762
default: help
@@ -41,9 +86,11 @@ format:
4186
@find ./ -name '*.go' -print0 | xargs -P 12 -0 -I '{}' goimports -w '{}'
4287

4388
# Test targets
44-
test:
89+
test: $(METRICS_DIR)
4590
@echo "Running tests..."
46-
@$(GO_CMD) test -race -cover -count=1 -parallel=10 $(MAIN_PACKAGES)
91+
@$(GO_CMD) test -race -covermode=atomic -coverprofile=$(COVERAGE_PROFILE) -coverpkg=$(COVERPKG) -count=1 -parallel=10 $(MAIN_PACKAGES)
92+
@$(GO_CMD) tool cover -func=$(COVERAGE_PROFILE) > $(COVERAGE_FUNC_REPORT)
93+
@echo "Coverage report written to $(COVERAGE_FUNC_REPORT)"
4794

4895
test_all: test test_integration
4996

@@ -76,6 +123,78 @@ test_update:
76123
@cp -fv cypher/models/pgsql/test/updated_cases/* cypher/models/pgsql/test/translation_cases
77124
@rm -rf cypher/models/pgsql/test/updated_cases
78125

126+
# Metric targets
127+
$(METRICS_DIR):
128+
@mkdir -p $(METRICS_DIR)
129+
130+
complexity: $(METRICS_DIR)
131+
@echo "Measuring cyclomatic complexity..."
132+
@$(GO_CMD) tool gocyclo -top $(CYCLO_TOP) -ignore '$(METRICS_IGNORE)' . | tee $(CYCLO_REPORT)
133+
@echo "Cyclomatic complexity report written to $(CYCLO_REPORT)"
134+
135+
complexity_check: $(METRICS_DIR)
136+
@echo "Checking cyclomatic complexity..."
137+
@if [ "$(METRICS_ENFORCE)" = "1" ]; then \
138+
$(GO_CMD) tool gocyclo -over $(CYCLO_OVER) -ignore '$(METRICS_IGNORE)' . | tee $(CYCLO_REPORT); \
139+
else \
140+
$(GO_CMD) tool gocyclo -top $(CYCLO_TOP) -ignore '$(METRICS_IGNORE)' . | tee $(CYCLO_REPORT); \
141+
echo "METRICS_ENFORCE=0; cyclomatic complexity threshold $(CYCLO_OVER) is report-only."; \
142+
fi
143+
144+
crap: test
145+
@echo "Calculating CRAP metrics..."
146+
@$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) $(QUALITY_INPUTS) -text $(CRAP_TEXT_REPORT) -json $(CRAP_JSON_REPORT) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT)
147+
@echo "CRAP and quality reports written to $(CRAP_TEXT_REPORT), $(CRAP_JSON_REPORT), $(QUALITY_TEXT_REPORT), $(QUALITY_JSON_REPORT), and $(METRICS_HTML_REPORT)"
148+
149+
crap_check: test
150+
@echo "Checking CRAP metrics..."
151+
@if [ "$(METRICS_ENFORCE)" = "1" ]; then \
152+
$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) $(QUALITY_INPUTS) -text $(CRAP_TEXT_REPORT) -json $(CRAP_JSON_REPORT) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT) -fail-over $(CRAP_OVER) -fail-quality; \
153+
else \
154+
$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) $(QUALITY_INPUTS) -text $(CRAP_TEXT_REPORT) -json $(CRAP_JSON_REPORT) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT); \
155+
echo "METRICS_ENFORCE=0; CRAP threshold $(CRAP_OVER) is report-only."; \
156+
fi
157+
158+
quality: test
159+
@echo "Calculating quality metrics..."
160+
@$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) $(QUALITY_INPUTS) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT) -stdout=false
161+
@echo "Quality reports written to $(QUALITY_TEXT_REPORT), $(QUALITY_JSON_REPORT), and $(METRICS_HTML_REPORT)"
162+
163+
quality_check: test
164+
@echo "Checking quality metrics..."
165+
@if [ "$(METRICS_ENFORCE)" = "1" ]; then \
166+
$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) $(QUALITY_INPUTS) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT) -stdout=false -fail-quality; \
167+
else \
168+
$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) $(QUALITY_INPUTS) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT) -stdout=false; \
169+
echo "METRICS_ENFORCE=0; quality watch signals are report-only."; \
170+
fi
171+
172+
quality_backend: test
173+
@echo "Running backend equivalence test captures..."
174+
@if [ -z "$(PG_CONNECTION_STRING)" ] || [ -z "$(NEO4J_CONNECTION_STRING)" ]; then \
175+
echo "PG_CONNECTION_STRING and NEO4J_CONNECTION_STRING are required."; \
176+
exit 1; \
177+
fi
178+
@set +e; \
179+
CONNECTION_STRING="$(PG_CONNECTION_STRING)" $(GO_CMD) test -json -tags 'manual_integration integration' -race -cover -count=1 -p=1 -parallel=1 $(MAIN_PACKAGES) > $(BACKEND_PG_REPORT); \
180+
pg_status=$$?; \
181+
CONNECTION_STRING="$(NEO4J_CONNECTION_STRING)" $(GO_CMD) test -json -tags 'manual_integration integration' -race -cover -count=1 -p=1 -parallel=1 $(MAIN_PACKAGES) > $(BACKEND_NEO4J_REPORT); \
182+
neo4j_status=$$?; \
183+
set -e; \
184+
$(GO_CMD) tool dawgs-metrics -source-root . -coverprofile $(COVERAGE_PROFILE) -ignore '$(METRICS_IGNORE)' -top $(CRAP_TOP) -over $(CRAP_OVER) -cyclo-over $(CYCLO_OVER) -backend-result pg=$(BACKEND_PG_REPORT) -backend-result neo4j=$(BACKEND_NEO4J_REPORT) $(QUALITY_INPUTS) -quality-text $(QUALITY_TEXT_REPORT) -quality-json $(QUALITY_JSON_REPORT) -html $(METRICS_HTML_REPORT) -stdout=false; \
185+
if [ $$pg_status -ne 0 ]; then exit $$pg_status; fi; \
186+
if [ $$neo4j_status -ne 0 ]; then exit $$neo4j_status; fi
187+
188+
quality_bench: $(METRICS_DIR)
189+
@echo "Running benchmark capture..."
190+
@$(GO_CMD) run ./cmd/benchmark -output $(QUALITY_BENCHMARK_MARKDOWN) -json-output $(QUALITY_BENCHMARK_REPORT)
191+
@echo "Benchmark reports written to $(QUALITY_BENCHMARK_MARKDOWN) and $(QUALITY_BENCHMARK_REPORT)"
192+
193+
metrics: complexity crap
194+
195+
metrics_check: METRICS_ENFORCE = 1
196+
metrics_check: complexity_check crap_check
197+
79198
# Utility targets
80199
generate:
81200
@echo "Running code generation..."
@@ -87,6 +206,7 @@ clean:
87206

88207
@rm -rf cypher/analyzer/updated_cases/
89208
@rm -rf cypher/models/pgsql/test/updated_cases
209+
@rm -rf $(METRICS_DIR)
90210

91211
help:
92212
@echo "Available targets:"
@@ -114,6 +234,13 @@ help:
114234
@echo " test_neo4j - Run Neo4j integration tests"
115235
@echo " test_pg - Run PostgreSQL integration tests"
116236
@echo " test_update - Update test cases"
237+
@echo " complexity - Report cyclomatic complexity"
238+
@echo " crap - Report CRAP scores from unit test coverage"
239+
@echo " quality - Report drift, equivalence, invariant, fuzz, mutation, and benchmark signals"
240+
@echo " quality_backend - Capture backend equivalence test results"
241+
@echo " quality_bench - Capture benchmark markdown and JSON reports"
242+
@echo " metrics - Run cyclomatic complexity, CRAP, and quality reports"
243+
@echo " metrics_check - Enforce cyclomatic complexity, CRAP, and quality thresholds"
117244
@echo ""
118245
@echo "Utility:"
119246
@echo " clean - Clean build artifacts"

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,50 @@ numbers.
8484
The integration benchmark runner includes committed `base` and `traversal_shapes` datasets by default. The traversal
8585
shape suite checks expected result counts for chain, fanout, bounded cycle, disconnected, edge-kind-selective, and
8686
multi-path shortest-path scenarios before recording timings.
87+
88+
### Test Metrics
89+
90+
`make test` writes unit test coverage artifacts under `.coverage/`:
91+
92+
```bash
93+
make test
94+
```
95+
96+
The stable coverage profile is `.coverage/unit.out`, and the function coverage summary is `.coverage/coverage.txt`.
97+
98+
Cyclomatic complexity, CRAP, and quality signal reports are available through dedicated metric targets:
99+
100+
```bash
101+
make complexity
102+
make crap
103+
make quality
104+
make metrics
105+
```
106+
107+
`make complexity` writes `.coverage/cyclomatic.txt`. `make crap` reruns unit tests for a fresh coverage profile, then
108+
writes `.coverage/crap.txt`, `.coverage/crap.json`, `.coverage/quality.txt`, `.coverage/quality.json`, and a standalone
109+
HTML report at `.coverage/metrics.html`. The quality section summarizes semantic drift, backend equivalence,
110+
integration/template invariants, fuzz health, mutation score, and benchmark drift. Signals that need external captures are
111+
reported as pending unless their input files are provided.
112+
Generated parser files, tests, vendor code, and testdata are excluded from these reports. The HTML report embeds its CSS
113+
and JavaScript directly in the document, so it can be opened without network access.
114+
115+
Optional quality inputs can be supplied through Make variables:
116+
117+
```bash
118+
make quality BACKEND_RESULT_ARGS="-backend-result pg=.coverage/integration-pg.json -backend-result neo4j=.coverage/integration-neo4j.json"
119+
make quality BENCHMARK_REPORT=.coverage/benchmark.json BENCHMARK_BASELINE=.coverage/benchmark-baseline.json
120+
make quality FUZZ_REPORT=.coverage/fuzz.json MUTATION_REPORT=.coverage/mutation.json
121+
```
122+
123+
`make quality_backend` captures PostgreSQL and Neo4j integration results for backend equivalence comparison. It requires
124+
`PG_CONNECTION_STRING` and `NEO4J_CONNECTION_STRING`. `make quality_bench` writes benchmark markdown and JSON captures
125+
for later baseline comparison.
126+
127+
Thresholds are report-only by default. To enforce the configured thresholds, run:
128+
129+
```bash
130+
make metrics_check
131+
```
132+
133+
The defaults can be adjusted with `CYCLO_TOP`, `CYCLO_OVER`, `CRAP_TOP`, `CRAP_OVER`, and `BENCHMARK_REGRESSION`.

cmd/benchmark/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Benchmark
22

3-
Runs query scenarios against a real database and outputs a markdown timing table.
3+
Runs query scenarios against a real database and outputs markdown, JSON, or benchfmt timing data.
44

55
## Usage
66

@@ -25,6 +25,9 @@ go run ./cmd/benchmark -connection "..." -output report.md
2525

2626
# Emit benchfmt for benchstat
2727
go run ./cmd/benchmark -connection "..." -format benchfmt -output report.bench
28+
29+
# Save markdown and JSON for quality baseline comparison
30+
go run ./cmd/benchmark -connection "..." -output report.md -json-output report.json
2831
```
2932

3033
## Flags
@@ -39,6 +42,7 @@ go run ./cmd/benchmark -connection "..." -format benchfmt -output report.bench
3942
| `-dataset-dir` | `integration/testdata` | Path to testdata directory |
4043
| `-format` | `markdown` | Output format (`markdown`, `json`, `benchfmt`) |
4144
| `-output` | stdout | Output file |
45+
| `-json-output` | | JSON output file for baseline comparison |
4246

4347
Use `-format benchfmt` when comparing scenario timings with `benchstat`. Each timed scenario iteration is emitted as a
4448
separate `ns/op` sample so two benchmark runs can be compared directly.

cmd/benchmark/main.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ func main() {
4040
driver = flag.String("driver", "pg", "database driver (pg, neo4j)")
4141
connStr = flag.String("connection", "", "database connection string (or CONNECTION_STRING)")
4242
iterations = flag.Int("iterations", 10, "timed iterations per scenario")
43-
output = flag.String("output", "", "markdown output file (default: stdout)")
43+
output = flag.String("output", "", "output file (default: stdout)")
4444
format = flag.String("format", reportFormatMarkdown, "output format (markdown, json, benchfmt)")
45+
jsonOutput = flag.String("json-output", "", "JSON output file for baseline comparison")
4546
datasetDir = flag.String("dataset-dir", "integration/testdata", "path to testdata directory")
4647
localDataset = flag.String("local-dataset", "", "additional local dataset (e.g. local/phantom)")
4748
onlyDataset = flag.String("dataset", "", "run only this dataset (e.g. diamond, local/phantom)")
@@ -161,26 +162,39 @@ func main() {
161162
}
162163
}
163164

164-
// Write report
165-
var mdOut *os.File
165+
// Write primary report.
166+
var out *os.File
166167
if *output != "" {
167168
var err error
168-
mdOut, err = os.Create(*output)
169+
out, err = os.Create(*output)
169170
if err != nil {
170171
fatal("failed to create output: %v", err)
171172
}
172-
defer mdOut.Close()
173+
defer out.Close()
173174
} else {
174-
mdOut = os.Stdout
175+
out = os.Stdout
175176
}
176177

177-
if err := writeReport(mdOut, report, *format); err != nil {
178+
if err := writeReport(out, report, *format); err != nil {
178179
fatal("failed to write report: %v", err)
179180
}
180181

181182
if *output != "" {
182183
fmt.Fprintf(os.Stderr, "wrote %s\n", *output)
183184
}
185+
186+
if *jsonOutput != "" {
187+
jsonOut, err := os.Create(*jsonOutput)
188+
if err != nil {
189+
fatal("failed to create JSON output: %v", err)
190+
}
191+
defer jsonOut.Close()
192+
193+
if err := writeJSON(jsonOut, report); err != nil {
194+
fatal("failed to write JSON output: %v", err)
195+
}
196+
fmt.Fprintf(os.Stderr, "wrote %s\n", *jsonOutput)
197+
}
184198
}
185199

186200
func scanKinds(datasetDir string, datasets []string) (graph.Kinds, graph.Kinds) {

cmd/benchmark/report.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ const (
3434

3535
// Report holds all benchmark results and metadata.
3636
type Report struct {
37-
Driver string
38-
GitRef string
39-
Date string
40-
Iterations int
41-
Results []Result
37+
Driver string `json:"driver"`
38+
GitRef string `json:"git_ref"`
39+
Date string `json:"date"`
40+
Iterations int `json:"iterations"`
41+
Results []Result `json:"results"`
4242
}
4343

4444
func writeReport(w io.Writer, r Report, format string) error {

cmd/benchmark/report_test.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ func TestWriteJSON(t *testing.T) {
3636

3737
require.NoError(t, writeReport(&out, report, reportFormatJSON))
3838

39-
require.Contains(t, out.String(), `"Driver": "pg"`)
40-
require.Contains(t, out.String(), `"Samples": [`)
39+
require.Contains(t, out.String(), `"driver": "pg"`)
40+
require.Contains(t, out.String(), `"samples": [`)
4141
require.Contains(t, out.String(), `1000000`)
4242
}
4343

@@ -96,3 +96,39 @@ func testReport() Report {
9696
}},
9797
}
9898
}
99+
100+
func TestWriteJSONEmitsBaselineFriendlyReport(t *testing.T) {
101+
report := Report{
102+
Driver: "pg",
103+
GitRef: "abc123",
104+
Date: "2026-05-14",
105+
Iterations: 3,
106+
Results: []Result{{
107+
Section: "Traversal",
108+
Dataset: "base",
109+
Label: "depth 1",
110+
Stats: Stats{
111+
Median: 10 * time.Millisecond,
112+
P95: 20 * time.Millisecond,
113+
Max: 30 * time.Millisecond,
114+
},
115+
}},
116+
}
117+
118+
var output bytes.Buffer
119+
if err := writeJSON(&output, report); err != nil {
120+
t.Fatalf("write JSON: %v", err)
121+
}
122+
123+
text := output.String()
124+
for _, expected := range []string{
125+
`"driver": "pg"`,
126+
`"git_ref": "abc123"`,
127+
`"median": 10000000`,
128+
`"section": "Traversal"`,
129+
} {
130+
if !strings.Contains(text, expected) {
131+
t.Fatalf("JSON report missing %q:\n%s", expected, text)
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)