From 942b59ac08d9e4632d4639caa4530c2f20fb505c Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Thu, 21 Aug 2025 17:55:12 +1000 Subject: [PATCH 1/4] feat: integrate JSON Schema Test Suite with blacklisting and coverage reporting --- .github/workflows/ci.yaml | 17 + .gitmodules | 3 + go.mod | 48 +++ go.sum | 134 ++++++++ jsonpointer/jsonpointer_test.go | 24 +- jsonpointer/models.go | 16 +- jsonpointer/models_yaml_fallback_test.go | 197 +++++++++++ jsonpointer/navigation.go | 9 +- jsonschema/oas3/jsonschema.go | 23 ++ jsonschema/oas3/resolution.go | 9 +- jsonschema/oas3/schema.go | 54 +++ jsonschema/oas3/tests/remote_server.go | 218 ++++++++++++ jsonschema/oas3/tests/testsuite | 1 + jsonschema/oas3/tests/testsuite_test.go | 415 +++++++++++++++++++++++ marshaller/populator.go | 79 ++++- marshaller/sequencedmap.go | 4 +- mise-tasks/setup-submodules | 27 ++ mise-tasks/test | 7 + references/reference.go | 11 +- values/eithervalue.go | 4 +- yml/yml.go | 1 + 21 files changed, 1267 insertions(+), 34 deletions(-) create mode 100644 .gitmodules create mode 100644 jsonpointer/models_yaml_fallback_test.go create mode 100644 jsonschema/oas3/tests/remote_server.go create mode 160000 jsonschema/oas3/tests/testsuite create mode 100644 jsonschema/oas3/tests/testsuite_test.go create mode 100755 mise-tasks/setup-submodules diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cfc5bd0..e99047d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + with: + submodules: recursive - name: Install mise uses: jdx/mise-action@v2 @@ -46,6 +48,8 @@ jobs: steps: - uses: actions/checkout@v5 + with: + submodules: recursive - name: Install mise uses: jdx/mise-action@v2 @@ -56,6 +60,14 @@ jobs: go-version-file: "go.mod" cache: true + # Verify Docker is available for testcontainers (Ubuntu only) + - name: Verify Docker availability + if: matrix.os == 'ubuntu-latest' + run: | + docker --version + docker info + echo "Docker is available for testcontainers" + - name: Get current date id: date run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT @@ -101,6 +113,11 @@ jobs: # Checkout main branch in a temporary directory git worktree add /tmp/main-branch main + # Initialize submodules in the main branch worktree + cd /tmp/main-branch + git submodule update --init --recursive || echo "Submodule initialization failed, continuing without submodules" + cd "$CURRENT_DIR" + # Run tests on main branch to get coverage (with timeout) cd /tmp/main-branch timeout 300 go test -coverprofile=main-coverage.out -covermode=atomic ./... > /dev/null 2>&1 || echo "Main branch tests failed or timed out" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6a149dd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "jsonschema/oas3/tests/testsuite"] + path = jsonschema/oas3/tests/testsuite + url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git diff --git a/go.mod b/go.mod index d514e02..a59e4a6 100644 --- a/go.mod +++ b/go.mod @@ -14,10 +14,58 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.2.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.5 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/testcontainers/testcontainers-go v0.38.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sys v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index 2f3c538..ab80c7e 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,116 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 h1:aRd8M7HJVZOqn/vhOzrGcQH0lNAMkqMn+pXUYkatmcA= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= +github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ= github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= @@ -42,20 +120,76 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= +github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/jsonpointer/jsonpointer_test.go b/jsonpointer/jsonpointer_test.go index 91cf06c..6cea6aa 100644 --- a/jsonpointer/jsonpointer_test.go +++ b/jsonpointer/jsonpointer_test.go @@ -50,6 +50,12 @@ func TestJSONPointer_Validate_Success(t *testing.T) { j: JSONPointer("/paths/~1special-events~1{eventId}/get"), }, }, + { + name: "empty tokens (consecutive slashes)", + args: args{ + j: JSONPointer("/some//path"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -94,11 +100,11 @@ func TestJSONPointer_Validate_Error(t *testing.T) { wantErr: errors.New("validation error -- jsonpointer part must be a valid token [^(?:[\x00-.0-}\x7f-\uffff]|~[01])+$]: /~/some~path"), }, { - name: "empty part in path", + name: "invalid path with unescaped tilde in middle", args: args{ - j: JSONPointer("/some//path"), + j: JSONPointer("/some/~invalid/path"), }, - wantErr: errors.New("validation error -- jsonpointer part must not be empty: /some//path"), + wantErr: errors.New("validation error -- jsonpointer part must be a valid token [^(?:[\x00-.0-}\x7f-\uffff]|~[01])+$]: /some/~invalid/path"), }, } for _, tt := range tests { @@ -273,6 +279,18 @@ func TestGetTarget_Success(t *testing.T) { }, want: 4, }, + { + name: "empty tokens in JSON pointer", + args: args{ + source: map[string]any{ + "": map[string]any{ + "": "value", + }, + }, + pointer: JSONPointer("//"), + }, + want: "value", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/jsonpointer/models.go b/jsonpointer/models.go index eb34acc..a21d31d 100644 --- a/jsonpointer/models.go +++ b/jsonpointer/models.go @@ -5,6 +5,7 @@ import ( "reflect" "github.com/speakeasy-api/openapi/internal/interfaces" + "github.com/speakeasy-api/openapi/marshaller" ) type model interface { @@ -110,7 +111,20 @@ func navigateModel(sourceVal reflect.Value, currentPart navigationPart, stack [] } if coreFieldIndex == -1 { - return nil, nil, ErrNotFound.Wrap(fmt.Errorf("key %s not found in core model at %s", currentPart.Value, currentPath)) + // Field not found in core model, try searching the associated YAML node + // Check if the model implements CoreModeler interface (which has GetRootNode) + if coreModeler, ok := coreAny.(marshaller.CoreModeler); ok { + rootNode := coreModeler.GetRootNode() + if rootNode != nil { + // Use the existing YAML node navigation logic to search for the key + result, newStack, err := getYamlNodeTarget(rootNode, currentPart, stack, currentPath, o) + if err == nil { + return result, newStack, nil + } + } + } + + return nil, nil, ErrNotFound.Wrap(fmt.Errorf("key %s not found in core model or YAML node at %s", currentPart.Value, currentPath)) } // Find the corresponding field in the high-level model diff --git a/jsonpointer/models_yaml_fallback_test.go b/jsonpointer/models_yaml_fallback_test.go new file mode 100644 index 0000000..7c1950e --- /dev/null +++ b/jsonpointer/models_yaml_fallback_test.go @@ -0,0 +1,197 @@ +package jsonpointer + +import ( + "reflect" + "testing" + + "github.com/speakeasy-api/openapi/marshaller" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// TestModel represents a simple model for testing YAML fallback +type TestModel struct { + marshaller.Model[TestModelCore] + + KnownField string +} + +type TestModelCore struct { + marshaller.CoreModel `model:"testModelCore"` + + KnownField marshaller.Node[string] `key:"knownField"` +} + +func TestNavigateModel_YAMLFallback_Success(t *testing.T) { + t.Parallel() + tests := []struct { + name string + yamlContent string + jsonPointer string + expectedType string + expectedVal interface{} + }{ + { + name: "unknown field in YAML root", + yamlContent: ` +knownField: "known value" +unknownField: "unknown value" +`, + jsonPointer: "/unknownField", + expectedType: "*yaml.Node", + expectedVal: "unknown value", + }, + { + name: "nested unknown field", + yamlContent: ` +knownField: "known value" +unknownObject: + nestedField: "nested value" +`, + jsonPointer: "/unknownObject/nestedField", + expectedType: "*yaml.Node", + expectedVal: "nested value", + }, + { + name: "unknown array field", + yamlContent: ` +knownField: "known value" +unknownArray: + - "item1" + - "item2" +`, + jsonPointer: "/unknownArray/1", + expectedType: "*yaml.Node", + expectedVal: "item2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Parse YAML content + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(tt.yamlContent), &rootNode) + require.NoError(t, err, "failed to parse YAML") + + // Create test model with YAML node + model := &TestModel{} + model.GetCore().SetRootNode(&rootNode) + + // Test navigation + pointer := JSONPointer(tt.jsonPointer) + result, err := GetTarget(model, pointer) + + require.NoError(t, err, "navigation should succeed") + require.NotNil(t, result, "result should not be nil") + + // Verify result type + assert.Equal(t, tt.expectedType, getTypeName(result), "result type should match expected") + + // For YAML nodes, check the value + if yamlNode, ok := result.(*yaml.Node); ok { + assert.Equal(t, tt.expectedVal, yamlNode.Value, "YAML node value should match expected") + } + }) + } +} + +func TestNavigateModel_YAMLFallback_KnownFieldStillWorks(t *testing.T) { + t.Parallel() + yamlContent := ` +knownField: "known value" +unknownField: "unknown value" +` + + // Parse YAML content + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(yamlContent), &rootNode) + require.NoError(t, err, "failed to parse YAML") + + // Create test model with YAML node + model := &TestModel{ + KnownField: "known value", + } + model.GetCore().SetRootNode(&rootNode) + + // Test navigation to known field (should use struct field, not YAML fallback) + pointer := JSONPointer("/knownField") + result, err := GetTarget(model, pointer) + + require.NoError(t, err, "navigation should succeed") + require.NotNil(t, result, "result should not be nil") + + // Should return the string field from the high-level model, not a YAML node + assert.Equal(t, "string", getTypeName(result), "should return struct field, not YAML fallback") +} + +func TestNavigateModel_YAMLFallback_Error(t *testing.T) { + t.Parallel() + tests := []struct { + name string + yamlContent string + jsonPointer string + }{ + { + name: "field not found anywhere", + yamlContent: ` +knownField: "known value" +`, + jsonPointer: "/nonExistentField", + }, + { + name: "nested field not found", + yamlContent: ` +knownField: "known value" +existingObject: + someField: "value" +`, + jsonPointer: "/existingObject/nonExistentField", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Parse YAML content + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(tt.yamlContent), &rootNode) + require.NoError(t, err, "failed to parse YAML") + + // Create test model with YAML node + model := &TestModel{} + model.GetCore().SetRootNode(&rootNode) + + // Test navigation + pointer := JSONPointer(tt.jsonPointer) + result, err := GetTarget(model, pointer) + + require.Error(t, err, "navigation should fail") + assert.Nil(t, result, "result should be nil") + assert.Contains(t, err.Error(), "not found", "error should indicate field not found") + }) + } +} + +func TestNavigateModel_YAMLFallback_NoYAMLNode(t *testing.T) { + t.Parallel() + // Create test model without YAML node + model := &TestModel{} + + // Test navigation to unknown field + pointer := JSONPointer("/unknownField") + result, err := GetTarget(model, pointer) + + require.Error(t, err, "navigation should fail when no YAML node available") + assert.Nil(t, result, "result should be nil") + assert.Contains(t, err.Error(), "not found", "error should indicate field not found") +} + +// Helper function to get type name for assertions +func getTypeName(v interface{}) string { + if v == nil { + return "nil" + } + return reflect.TypeOf(v).String() +} diff --git a/jsonpointer/navigation.go b/jsonpointer/navigation.go index 47477c6..b6eea04 100644 --- a/jsonpointer/navigation.go +++ b/jsonpointer/navigation.go @@ -54,15 +54,12 @@ func (j JSONPointer) getNavigationStack() ([]navigationPart, error) { strParts := strings.Split(strings.TrimPrefix(string(j), "/"), "/") for _, part := range strParts { - if len(part) == 0 { - return nil, fmt.Errorf("jsonpointer part must not be empty: %s", string(j)) - } - - if !tokenRegex.MatchString(part) { + // Empty parts are valid according to RFC 6901 - they represent empty string keys + if len(part) > 0 && !tokenRegex.MatchString(part) { return nil, fmt.Errorf("jsonpointer part must be a valid token [%s]: %s", tokenRegex.String(), string(j)) } - if digitOnlyRegex.MatchString(part) && (len(part) == 1 || part[0] != '0') { + if len(part) > 0 && digitOnlyRegex.MatchString(part) && (len(part) == 1 || part[0] != '0') { stack = append(stack, navigationPart{ Type: partTypeIndex, Value: part, diff --git a/jsonschema/oas3/jsonschema.go b/jsonschema/oas3/jsonschema.go index d65ef09..ca0af73 100644 --- a/jsonschema/oas3/jsonschema.go +++ b/jsonschema/oas3/jsonschema.go @@ -251,3 +251,26 @@ func (j *JSONSchema[T]) ShallowCopy() *JSONSchema[T] { return result } + +// PopulateWithParent implements the ParentAwarePopulator interface to establish parent relationships during population +func (j *JSONSchema[T]) PopulateWithParent(source any, parent any) error { + // If we have a parent that is also a JSONSchema, establish the parent relationship + if parent != nil { + if parentSchema, ok := parent.(*Schema); ok { + j.SetParent(parentSchema.GetParent()) + // If the parent has a top-level parent, inherit it; otherwise, the parent is the top-level + if parentSchema.GetParent().GetTopLevelParent() != nil { + j.SetTopLevelParent(parentSchema.GetParent().GetTopLevelParent()) + } else { + j.SetTopLevelParent(parentSchema.GetParent()) + } + } + } + + // First, perform the standard population + if err := j.EitherValue.PopulateWithParent(source, j); err != nil { + return err + } + + return nil +} diff --git a/jsonschema/oas3/resolution.go b/jsonschema/oas3/resolution.go index bcda84d..9f369ec 100644 --- a/jsonschema/oas3/resolution.go +++ b/jsonschema/oas3/resolution.go @@ -153,9 +153,14 @@ func (s *JSONSchema[Referenceable]) resolve(ctx context.Context, opts references // Special case: detect self-referencing schemas (references to root document) // This catches cases like "#" which reference the root document itself + // Only consider it circular if this schema has no parent (i.e., it's at the root level) if ref.GetURI() == "" && ref.GetJSONPointer() == "" { - s.circularErrorFound = true - return nil, nil, errors.New("circular reference detected: self-referencing schema") + // Check if this schema has a parent - if it does, then referencing "#" is legitimate + // If it has no parent, then it's the root schema referencing itself, which is circular + if s.GetParent() == nil && s.GetTopLevelParent() == nil { + s.circularErrorFound = true + return nil, nil, errors.New("circular reference detected: self-referencing schema") + } } // Check for circular reference by looking for the current reference in the chain diff --git a/jsonschema/oas3/schema.go b/jsonschema/oas3/schema.go index c04ebaa..334441d 100644 --- a/jsonschema/oas3/schema.go +++ b/jsonschema/oas3/schema.go @@ -3,6 +3,7 @@ package oas3 import ( _ "embed" + "fmt" "reflect" "github.com/speakeasy-api/openapi/extensions" @@ -72,6 +73,11 @@ type Schema struct { Schema *string XML *XML Extensions *extensions.Extensions + + // Parent reference links - private fields to avoid serialization + // These are set when the schema was populated as a child of another schema. + // Used for circular reference detection during resolution. + parent *JSONSchema[Referenceable] // Immediate parent schema in the hierarchy } // ShallowCopy creates a shallow copy of the Schema. @@ -127,6 +133,7 @@ func (s *Schema) ShallowCopy() *Schema { Schema: s.Schema, XML: s.XML, Extensions: s.Extensions, + parent: s.parent, } // Shallow copy slices - create new slice but reference same elements @@ -869,6 +876,53 @@ func (s *Schema) IsEqual(other *Schema) bool { return true } +// GetParent returns the immediate parent JSONSchema if this schema was populated as a child of another schema. +// Returns nil if this schema has no parent or was not populated via parent-aware population. +func (s *Schema) GetParent() *JSONSchema[Referenceable] { + if s == nil { + return nil + } + return s.parent +} + +// SetParent sets the immediate parent JSONSchema for this schema. +// This is used during parent-aware population to establish parent relationships. +func (s *Schema) SetParent(parent *JSONSchema[Referenceable]) { + if s == nil { + return + } + s.parent = parent +} + +// PopulateWithParent implements the ParentAwarePopulator interface to establish parent relationships during population +func (s *Schema) PopulateWithParent(source any, parent any) error { + // If we have a parent that is a JSONSchema, establish the parent relationship + if parent != nil { + if parentSchema, ok := parent.(*JSONSchema[Referenceable]); ok { + s.SetParent(parentSchema) + } + } + + var coreSchema *core.Schema + switch src := source.(type) { + case *core.Schema: + coreSchema = src + case core.Schema: + coreSchema = &src + default: + return fmt.Errorf("expected *core.Reference[C] or core.Reference[C], got %T", source) + } + + // First, perform the standard population + if err := marshaller.PopulateModel(source, s); err != nil { + return err + } + + s.SetCore(coreSchema) + + return nil +} + // Helper functions for equality comparison func equalJSONSchemas(a, b *JSONSchema[Referenceable]) bool { diff --git a/jsonschema/oas3/tests/remote_server.go b/jsonschema/oas3/tests/remote_server.go new file mode 100644 index 0000000..b8c5f7c --- /dev/null +++ b/jsonschema/oas3/tests/remote_server.go @@ -0,0 +1,218 @@ +package tests + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// RemoteServer manages a testcontainer that serves the JSON Schema Test Suite remote files +type RemoteServer struct { + container testcontainers.Container + baseURL string +} + +// startRemoteServer starts a container serving the remote files at localhost:1234 +func startRemoteServer() (*RemoteServer, error) { + ctx := context.Background() + + // Get the absolute path to the remotes directory + remotesPath, err := filepath.Abs("testsuite/remotes") + if err != nil { + return nil, fmt.Errorf("failed to get absolute path to remotes directory: %w", err) + } + + // Check if remotes directory exists + if _, err := os.Stat(remotesPath); os.IsNotExist(err) { + return nil, fmt.Errorf("remotes directory does not exist: %s", remotesPath) + } + + // Create nginx configuration that enables directory indexing and proper file serving + nginxConfig := ` +server { + listen 80; + server_name localhost; + root /remotes; + + # Enable directory indexing + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + + # Serve files with proper MIME types + location / { + try_files $uri $uri/ =404; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range"; + } + + # Specific handling for JSON files + location ~* \.json$ { + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + } +} +` + + // Create container request with fixed port binding to 1234 + req := testcontainers.ContainerRequest{ + Image: "nginx:alpine", + ExposedPorts: []string{"80/tcp"}, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: remotesPath, + ContainerFilePath: "/remotes", + FileMode: 0755, + }, + { + HostFilePath: "", + ContainerFilePath: "/etc/nginx/conf.d/default.conf", + FileMode: 0644, + Reader: strings.NewReader(nginxConfig), + }, + }, + WaitingFor: wait.ForHTTP("/draft2020-12/integer.json").WithPort("80/tcp").WithStartupTimeout(30 * time.Second), + } + + // Start the container with port binding + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to start remote server container: %w", err) + } + + // Get the mapped port + mappedPort, err := container.MappedPort(ctx, "80") + if err != nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("failed to get mapped port: %w (cleanup error: %w)", err, termErr) + } + return nil, fmt.Errorf("failed to get mapped port: %w", err) + } + + // Get the host + host, err := container.Host(ctx) + if err != nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("failed to get container host: %w (cleanup error: %w)", err, termErr) + } + return nil, fmt.Errorf("failed to get container host: %w", err) + } + + baseURL := fmt.Sprintf("http://%s:%s", host, mappedPort.Port()) + + // Verify the server is working by checking a known file + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(baseURL + "/draft2020-12/integer.json") + if err != nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("failed to verify remote server is working: %w (cleanup error: %w)", err, termErr) + } + return nil, fmt.Errorf("failed to verify remote server is working: %w", err) + } + if resp == nil { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("received nil response from remote server (cleanup error: %w)", termErr) + } + return nil, errors.New("received nil response from remote server") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if termErr := container.Terminate(ctx); termErr != nil { + return nil, fmt.Errorf("remote server health check failed with status: %d (cleanup error: %w)", resp.StatusCode, termErr) + } + return nil, fmt.Errorf("remote server health check failed with status: %d", resp.StatusCode) + } + + return &RemoteServer{ + container: container, + baseURL: baseURL, + }, nil +} + +// GetBaseURL returns the base URL where the remote files are served +func (rs *RemoteServer) GetBaseURL() string { + return rs.baseURL +} + +// Stop stops and removes the container +func (rs *RemoteServer) Stop() { + if rs.container != nil { + ctx := context.Background() + err := rs.container.Terminate(ctx) + if err != nil { + // Use fmt.Printf since we can't access log in this context + fmt.Printf("Warning: failed to terminate remote server container: %v\n", err) + } + } +} + +// GetExpectedURL returns the URL that the test suite expects for a given path +// Since we're binding to localhost:1234, this is the same as GetActualURL +func (rs *RemoteServer) GetExpectedURL(path string) string { + return "http://localhost:1234/" + path +} + +// GetActualURL returns the actual URL where the file is served +func (rs *RemoteServer) GetActualURL(path string) string { + return rs.baseURL + "/" + path +} + +// GetHTTPClient returns an HTTP client that redirects localhost:1234 requests to the actual container +func (rs *RemoteServer) GetHTTPClient() *http.Client { + return &http.Client{ + Transport: &redirectTransport{ + baseURL: rs.baseURL, + base: http.DefaultTransport, + }, + Timeout: 30 * time.Second, + } +} + +// redirectTransport is an HTTP transport that redirects localhost:1234 requests to the actual container URL +type redirectTransport struct { + baseURL string + base http.RoundTripper +} + +func (rt *redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Check if this is a localhost:1234 request + if req.URL.Host == "localhost:1234" { + // Parse the container base URL + containerURL, err := url.Parse(rt.baseURL) + if err != nil { + return nil, fmt.Errorf("failed to parse container URL: %w", err) + } + + // Create a new URL with the container host/port but keep the original path + newURL := &url.URL{ + Scheme: containerURL.Scheme, + Host: containerURL.Host, + Path: req.URL.Path, + RawQuery: req.URL.RawQuery, + Fragment: req.URL.Fragment, + } + + // Clone the request with the new URL + newReq := req.Clone(req.Context()) + newReq.URL = newURL + + return rt.base.RoundTrip(newReq) + } + + // For all other requests, use the base transport + return rt.base.RoundTrip(req) +} diff --git a/jsonschema/oas3/tests/testsuite b/jsonschema/oas3/tests/testsuite new file mode 160000 index 0000000..15e4505 --- /dev/null +++ b/jsonschema/oas3/tests/testsuite @@ -0,0 +1 @@ +Subproject commit 15e4505bf689de5d30c29d50782bb48fa465c93f diff --git a/jsonschema/oas3/tests/testsuite_test.go b/jsonschema/oas3/tests/testsuite_test.go new file mode 100644 index 0000000..f8ee816 --- /dev/null +++ b/jsonschema/oas3/tests/testsuite_test.go @@ -0,0 +1,415 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/speakeasy-api/openapi/jsonschema/oas3" + "github.com/speakeasy-api/openapi/marshaller" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCase represents a single test case from the JSON Schema Test Suite +type TestCase struct { + Description string `json:"description"` + Comment string `json:"comment,omitempty"` + Schema json.RawMessage `json:"schema"` + Tests []Test `json:"tests"` +} + +// Test represents a single test within a test case +type Test struct { + Description string `json:"description"` + Comment string `json:"comment,omitempty"` + Data interface{} `json:"data"` + Valid bool `json:"valid"` +} + +// Blacklisted test files that we don't support or want to skip +// TODO work on improving support for these files +var blacklistedFiles = map[string]string{ + // Anchor resolution edge cases + "optional/anchor.json": "contains edge cases for anchor resolution", + "anchor.json": "contains edge cases for anchor resolution", + + // Unknown keyword and ID edge cases + "optional/unknownKeyword.json": "contains edge cases for unknown keyword handling", + "optional/id.json": "contains edge cases for ID resolution", +} + +// Blacklisted test cases within specific files +// Key format: "filename:case_number" +// TODO work on improving support for these test cases +var blacklistedTestCases = map[string]string{ + // Remote reference tests that require $id base URI change support + "refRemote.json:2": "requires anchor resolution support", + "refRemote.json:4": "requires $id base URI change support", + "refRemote.json:5": "requires $id base URI change support", + "refRemote.json:6": "requires $id base URI change support", + "refRemote.json:7": "requires $id base URI change support", + "refRemote.json:8": "requires external reference resolution", + "refRemote.json:9": "requires anchor resolution support", + "refRemote.json:10": "requires $id base URI change support", + "refRemote.json:13": "requires nested absolute reference resolution", + "refRemote.json:14": "requires detached anchor resolution support", + + // ref.json tests that require advanced reference resolution features + "ref.json:11": "requires external reference resolution with $id", + "ref.json:15": "requires relative URI resolution with $id", + "ref.json:16": "requires absolute URI resolution with $id", + "ref.json:17": "requires complex $id resolution chain", + "ref.json:18": "requires $id evaluation before $ref", + "ref.json:19": "requires $id and $anchor evaluation before $ref", + "ref.json:20": "requires URN scheme support", + "ref.json:25": "requires URN scheme with JSON pointer", + "ref.json:26": "requires URN scheme with anchor", + "ref.json:27": "requires URN scheme with nested references", + "ref.json:28": "requires conditional schema reference resolution", + "ref.json:29": "requires conditional schema reference resolution", + "ref.json:30": "requires conditional schema reference resolution", + "ref.json:31": "requires absolute path reference resolution", + + // dynamicRef.json tests - all failing due to lack of dynamic reference support + "dynamicRef.json:0": "requires dynamic reference resolution support", + "dynamicRef.json:2": "requires dynamic reference resolution support", + "dynamicRef.json:3": "requires dynamic reference resolution support", + "dynamicRef.json:4": "requires dynamic reference resolution support", + "dynamicRef.json:5": "requires dynamic reference resolution support", + "dynamicRef.json:6": "requires dynamic reference resolution support", + "dynamicRef.json:7": "requires dynamic reference resolution support", + "dynamicRef.json:8": "requires dynamic reference resolution support", + "dynamicRef.json:9": "requires dynamic reference resolution support", + "dynamicRef.json:10": "requires dynamic reference resolution support", + "dynamicRef.json:11": "requires dynamic reference resolution support", + "dynamicRef.json:12": "requires dynamic reference resolution support", + "dynamicRef.json:13": "requires dynamic reference resolution support", + "dynamicRef.json:14": "requires dynamic reference resolution support", + "dynamicRef.json:15": "requires dynamic reference resolution support", + "dynamicRef.json:16": "requires dynamic reference resolution support", + "dynamicRef.json:19": "requires dynamic reference resolution support", + + // optional/dynamicRef.json tests + "optional/dynamicRef.json:0": "requires dynamic reference resolution support", + + // unevaluatedItems.json tests with dynamicRef + "unevaluatedItems.json:18": "requires dynamic reference resolution support", + + // unevaluatedProperties.json tests with dynamicRef + "unevaluatedProperties.json:21": "requires dynamic reference resolution support", +} + +const testSuiteDir = "testsuite/tests/draft2020-12" + +// Global variable to hold the remote server instance +var remoteServer *RemoteServer + +func TestMain(m *testing.M) { + // Check if the git submodule is initialized + if !isSubmoduleInitialized(testSuiteDir) { + log.Println("JSON Schema Test Suite submodule not initialized. Run 'git submodule update --init --recursive' to enable these tests.") + return + } + + // Start the remote server for remote reference tests + var err error + remoteServer, err = startRemoteServer() + if err != nil { + log.Printf("Warning: Failed to start remote server for remote reference tests: %v", err) + log.Println("Remote reference tests will be skipped.") + } else { + log.Printf("Remote server started at %s", remoteServer.GetBaseURL()) + } + + // Run tests + exitCode := m.Run() + + // Clean up the remote server + if remoteServer != nil { + remoteServer.Stop() + } + + os.Exit(exitCode) +} + +// TestJSONSchemaTestSuite_RoundTrip runs the JSON Schema Test Suite tests +// focusing on schema parsing, validation, reference resolution, and round-trip marshalling +func TestJSONSchemaTestSuite_RoundTrip(t *testing.T) { + t.Parallel() + + // Get all test files + testFiles := getAllTestFiles(t, testSuiteDir) + + // Track coverage statistics + var totalFiles, skippedFiles, totalCases, skippedCases, passedCases int + + for _, testFile := range testFiles { + totalFiles++ + + // Check if this file is blacklisted + if reason, isBlacklisted := blacklistedFiles[testFile]; isBlacklisted { + skippedFiles++ + t.Run(testFile, func(t *testing.T) { + t.Skipf("Skipping blacklisted file: %s", reason) + }) + continue + } + + t.Run(testFile, func(t *testing.T) { + t.Parallel() + fileCases, fileSkipped, filePassed := runRoundTripTestFile(t, filepath.Join(testSuiteDir, testFile)) + totalCases += fileCases + skippedCases += fileSkipped + passedCases += filePassed + }) + } + + // Print coverage summary + t.Cleanup(func() { + printCoverageSummary(t, totalFiles, skippedFiles, totalCases, skippedCases, passedCases) + }) +} + +// getAllTestFiles returns all JSON test files in the test suite directory +func getAllTestFiles(t *testing.T, testSuiteDir string) []string { + t.Helper() + var testFiles []string + + err := filepath.WalkDir(testSuiteDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if strings.HasSuffix(path, ".json") { + // Get relative path from testSuiteDir + relPath, err := filepath.Rel(testSuiteDir, path) + if err != nil { + return err + } + testFiles = append(testFiles, relPath) + } + + return nil + }) + + require.NoError(t, err) + return testFiles +} + +// runRoundTripTestFile runs all test cases in a single test file with round-trip testing +// Returns: totalCases, skippedCases, passedCases +func runRoundTripTestFile(t *testing.T, testFilePath string) (int, int, int) { + t.Helper() + // Read the test file + data, err := os.ReadFile(testFilePath) + require.NoError(t, err, "failed to read test file: %s", testFilePath) + + // Parse the test cases + var testCases []TestCase + err = json.Unmarshal(data, &testCases) + require.NoError(t, err, "failed to parse test file: %s", testFilePath) + + var skippedCases, passedCases int + + // Run each test case + for i, testCase := range testCases { + t.Run(fmt.Sprintf("case_%d_%s", i, sanitizeTestName(testCase.Description)), func(t *testing.T) { + // Check if this specific test case is blacklisted + fileName := filepath.Base(testFilePath) + testCaseKey := fmt.Sprintf("%s:%d", fileName, i) + if reason, isBlacklisted := blacklistedTestCases[testCaseKey]; isBlacklisted { + skippedCases++ + t.Skipf("Skipping blacklisted test case: %s", reason) + return + } + + runRoundTripTestCase(t, testCase, testFilePath) + passedCases++ + }) + } + + return len(testCases), skippedCases, passedCases +} + +// runRoundTripTestCase runs a single test case with round-trip testing +func runRoundTripTestCase(t *testing.T, testCase TestCase, testFilePath string) { + t.Helper() + ctx := t.Context() + + // Step 1: Unmarshal the schema + var schema oas3.JSONSchema[oas3.Referenceable] + validationErrs, err := marshaller.Unmarshal(ctx, bytes.NewReader(testCase.Schema), &schema) + require.NoError(t, err, "failed to unmarshal schema for test case: %s", testCase.Description) + require.Empty(t, validationErrs, "schema validation errors for test case: %s", testCase.Description) + + // Step 2: Run validation on the schema + schemaValidationErrs := schema.Validate(ctx) + require.Empty(t, schemaValidationErrs, "schema validation failed for test case: %s", testCase.Description) + + // Step 4: Use the Walk API to walk through the schema and resolve any references + err = walkAndResolveReferences(ctx, t, &schema, testFilePath) + require.NoError(t, err, "failed to walk and resolve references for test case: %s", testCase.Description) + + // Step 5: Marshal the schema back to JSON + var buf bytes.Buffer + err = marshaller.Marshal(ctx, &schema, &buf) + require.NoError(t, err, "failed to marshal schema for test case: %s", testCase.Description) + + // Step 6: Use assert.JSONEq to check the schema makes a successful round trip + originalJSON := string(testCase.Schema) + roundTripJSON := buf.String() + + // For round-trip comparison, we need to normalize both JSONs since the order might differ + // and some fields might be added/removed during the process + assert.JSONEq(t, originalJSON, roundTripJSON, "schema round-trip failed for test case: %s", testCase.Description) + + // Log success information + t.Logf("โœ… Round-trip successful for test case: %s", testCase.Description) + t.Logf(" Original schema size: %d bytes", len(originalJSON)) + t.Logf(" Round-trip schema size: %d bytes", len(roundTripJSON)) +} + +// walkAndResolveReferences walks through the schema using the Walk API and resolves any references +func walkAndResolveReferences(ctx context.Context, t *testing.T, schema *oas3.JSONSchema[oas3.Referenceable], testFilePath string) error { + t.Helper() + if schema == nil { + return nil + } + + // Walk through the schema and resolve any references we find + for item := range oas3.Walk(ctx, schema) { + err := item.Match(oas3.SchemaMatcher{ + Schema: func(s *oas3.JSONSchema[oas3.Referenceable]) error { + // If this is a reference, try to resolve it + if s.IsReference() { + // Create resolve options + resolveOpts := oas3.ResolveOptions{ + TargetLocation: testFilePath, + RootDocument: schema, + } + + // If we have a remote server running, use its custom HTTP client + if remoteServer != nil { + resolveOpts.HTTPClient = remoteServer.GetHTTPClient() + } + + // Attempt to resolve the reference + // Most test suite schemas should have resolvable references within the schema + vErrs, resolveErr := s.Resolve(ctx, resolveOpts) + + assert.NoError(t, resolveErr) + assert.Empty(t, vErrs) + } + return nil + }, + }) + if err != nil { + return fmt.Errorf("failed to process schema during walk: %w", err) + } + } + + return nil +} + +// sanitizeTestName sanitizes a test name for use as a Go test name +func sanitizeTestName(name string) string { + // Replace spaces and special characters with underscores + name = strings.ReplaceAll(name, " ", "_") + name = strings.ReplaceAll(name, "-", "_") + name = strings.ReplaceAll(name, "(", "_") + name = strings.ReplaceAll(name, ")", "_") + name = strings.ReplaceAll(name, "[", "_") + name = strings.ReplaceAll(name, "]", "_") + name = strings.ReplaceAll(name, "{", "_") + name = strings.ReplaceAll(name, "}", "_") + name = strings.ReplaceAll(name, ".", "_") + name = strings.ReplaceAll(name, ",", "_") + name = strings.ReplaceAll(name, ":", "_") + name = strings.ReplaceAll(name, ";", "_") + name = strings.ReplaceAll(name, "!", "_") + name = strings.ReplaceAll(name, "?", "_") + name = strings.ReplaceAll(name, "'", "_") + name = strings.ReplaceAll(name, "\"", "_") + name = strings.ReplaceAll(name, "/", "_") + name = strings.ReplaceAll(name, "\\", "_") + name = strings.ReplaceAll(name, "<", "_") + name = strings.ReplaceAll(name, ">", "_") + name = strings.ReplaceAll(name, "=", "_") + name = strings.ReplaceAll(name, "+", "_") + name = strings.ReplaceAll(name, "*", "_") + name = strings.ReplaceAll(name, "&", "_") + name = strings.ReplaceAll(name, "%", "_") + name = strings.ReplaceAll(name, "$", "_") + name = strings.ReplaceAll(name, "#", "_") + name = strings.ReplaceAll(name, "@", "_") + + // Remove multiple consecutive underscores + for strings.Contains(name, "__") { + name = strings.ReplaceAll(name, "__", "_") + } + + // Trim leading and trailing underscores + name = strings.Trim(name, "_") + + // Ensure the name is not empty + if name == "" { + name = "unnamed_test" + } + + return name +} + +// isSubmoduleInitialized checks if the git submodule is properly initialized +func isSubmoduleInitialized(testSuiteDir string) bool { + // Check if the directory exists and contains expected files + if _, err := os.Stat(testSuiteDir); os.IsNotExist(err) { + return false + } + + // Check if the directory contains test files (should have .json files) + entries, err := os.ReadDir(testSuiteDir) + if err != nil { + return false + } + + // Look for at least one .json file to confirm the submodule is initialized + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { + return true + } + } + + return false +} + +// printCoverageSummary prints a summary of test coverage statistics +func printCoverageSummary(t *testing.T, totalFiles, skippedFiles, totalCases, skippedCases, passedCases int) { + t.Helper() + + runFiles := totalFiles - skippedFiles + filesCoverage := float64(runFiles) / float64(totalFiles) * 100 + + casesCoverage := float64(passedCases) / float64(totalCases) * 100 + + t.Logf("\n"+ + "๐Ÿ“Š JSON Schema Test Suite Coverage Summary\n"+ + "==========================================\n"+ + "Files: %d/%d (%.1f%%) - %d skipped\n"+ + "Test Cases: %d/%d (%.1f%%) - %d skipped\n"+ + "Status: %d passed, %d skipped, %d total\n", + runFiles, totalFiles, filesCoverage, skippedFiles, + passedCases, totalCases, casesCoverage, skippedCases, + passedCases, skippedCases, totalCases) +} diff --git a/marshaller/populator.go b/marshaller/populator.go index cf79fad..ce97bd5 100644 --- a/marshaller/populator.go +++ b/marshaller/populator.go @@ -10,21 +10,26 @@ import ( // Pre-computed reflection types for performance var ( - nodeAccessorType = reflect.TypeOf((*NodeAccessor)(nil)).Elem() - populatorType = reflect.TypeOf((*Populator)(nil)).Elem() - sequencedMapType = reflect.TypeOf((*interfaces.SequencedMapInterface)(nil)).Elem() - coreModelerType = reflect.TypeOf((*CoreModeler)(nil)).Elem() - yamlNodePtrType = reflect.TypeOf((*yaml.Node)(nil)) - yamlNodeType = reflect.TypeOf(yaml.Node{}) - yamlNodePtrPtrType = reflect.TypeOf((**yaml.Node)(nil)) - populatorValueTag = "populatorValue" - populatorValueTrue = "true" + nodeAccessorType = reflect.TypeOf((*NodeAccessor)(nil)).Elem() + populatorType = reflect.TypeOf((*Populator)(nil)).Elem() + parentAwarePopulatorType = reflect.TypeOf((*ParentAwarePopulator)(nil)).Elem() + sequencedMapType = reflect.TypeOf((*interfaces.SequencedMapInterface)(nil)).Elem() + coreModelerType = reflect.TypeOf((*CoreModeler)(nil)).Elem() + yamlNodePtrType = reflect.TypeOf((*yaml.Node)(nil)) + yamlNodeType = reflect.TypeOf(yaml.Node{}) + yamlNodePtrPtrType = reflect.TypeOf((**yaml.Node)(nil)) + populatorValueTag = "populatorValue" + populatorValueTrue = "true" ) type Populator interface { Populate(source any) error } +type ParentAwarePopulator interface { + PopulateWithParent(source any, parent any) error +} + func Populate(source any, target any) error { t := reflect.ValueOf(target) @@ -59,10 +64,47 @@ func Populate(source any, target any) error { } } - return populateValue(source, t) + return populateValueWithParent(source, t, nil) +} + +func PopulateWithParent(source any, target any, parent any) error { + t := reflect.ValueOf(target) + + if t.Kind() == reflect.Ptr && t.IsNil() { + t.Set(CreateInstance(t.Type().Elem())) + } + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + s := reflect.ValueOf(source) + if s.Type().Implements(nodeAccessorType) { + source = source.(NodeAccessor).GetValue() + } + + // Special case for yaml.Node conversion (similar to unmarshaller.go:216-223) + switch { + case t.Type() == yamlNodePtrType: + if node, ok := source.(yaml.Node); ok { + t.Set(reflect.ValueOf(&node)) + return nil + } + case t.Type() == yamlNodeType: + if node, ok := source.(*yaml.Node); ok { + t.Set(reflect.ValueOf(*node)) + return nil + } + case t.Type() == yamlNodePtrPtrType: + if node, ok := source.(*yaml.Node); ok { + t.Set(reflect.ValueOf(&node)) + return nil + } + } + + return populateValueWithParent(source, t, parent) } -func populateModel(source any, target any) error { +func PopulateModel(source any, target any) error { s := reflect.ValueOf(source) t := reflect.ValueOf(target) @@ -113,7 +155,7 @@ func populateModel(source any, target any) error { if field.Anonymous { if targetSeqMap := getSequencedMapInterface(tField); targetSeqMap != nil { sourceForPopulation := getSourceForPopulation(s.Field(i), fieldInt) - if err := populateSequencedMap(sourceForPopulation, targetSeqMap); err != nil { + if err := populateSequencedMap(sourceForPopulation, targetSeqMap, target); err != nil { return err } } @@ -160,7 +202,7 @@ func populateModel(source any, target any) error { nodeValue = nodeAccessor.GetValue() } - if err := populateValue(nodeValue, tField); err != nil { + if err := populateValueWithParent(nodeValue, tField, target); err != nil { return err } } @@ -168,7 +210,7 @@ func populateModel(source any, target any) error { return nil } -func populateValue(source any, target reflect.Value) error { +func populateValueWithParent(source any, target reflect.Value, parent any) error { value := reflect.ValueOf(source) // Handle nil source early - when source is nil, reflect.ValueOf returns a zero Value @@ -208,18 +250,21 @@ func populateValue(source any, target reflect.Value) error { } targetType := target.Type() + if targetType.Implements(parentAwarePopulatorType) { + return target.Interface().(ParentAwarePopulator).PopulateWithParent(value.Interface(), parent) + } if targetType.Implements(populatorType) { return target.Interface().(Populator).Populate(value.Interface()) } // Check if target is a sequenced map and handle it specially if targetType.Implements(sequencedMapType) && !isEmbeddedSequencedMapType(value.Type()) { - return populateSequencedMap(value.Interface(), target.Interface().(interfaces.SequencedMapInterface)) + return populateSequencedMap(value.Interface(), target.Interface().(interfaces.SequencedMapInterface), parent) } // Check if target implements CoreSetter interface if coreSetter, ok := target.Interface().(CoreSetter); ok { - if err := populateModel(value.Interface(), target.Interface()); err != nil { + if err := PopulateModel(value.Interface(), target.Interface()); err != nil { return err } @@ -252,7 +297,7 @@ func populateValue(source any, target reflect.Value) error { } } - if err := populateValue(elementValue, target.Index(i)); err != nil { + if err := populateValueWithParent(elementValue, target.Index(i), target.Interface()); err != nil { return err } } diff --git a/marshaller/sequencedmap.go b/marshaller/sequencedmap.go index 36eaa44..24f4f21 100644 --- a/marshaller/sequencedmap.go +++ b/marshaller/sequencedmap.go @@ -112,7 +112,7 @@ func unmarshalSequencedMap(ctx context.Context, parentName string, node *yaml.No } // populateSequencedMap populates a target sequenced map from a source sequenced map -func populateSequencedMap(source any, target interfaces.SequencedMapInterface) error { +func populateSequencedMap(source any, target interfaces.SequencedMapInterface, parent any) error { if source == nil { return nil } @@ -154,7 +154,7 @@ func populateSequencedMap(source any, target interfaces.SequencedMapInterface) e targetValue = CreateInstance(valueType).Interface() } - if err := Populate(value, targetValue); err != nil { + if err := PopulateWithParent(value, targetValue, parent); err != nil { return err } diff --git a/mise-tasks/setup-submodules b/mise-tasks/setup-submodules new file mode 100755 index 0000000..cea9d5f --- /dev/null +++ b/mise-tasks/setup-submodules @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "๐Ÿ”ง Setting up git submodules..." + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo "โŒ Error: Not in a git repository" + exit 1 +fi + +# Initialize and update submodules +echo "๐Ÿ“ฅ Initializing and updating git submodules..." +git submodule update --init --recursive + +# Verify the JSON Schema Test Suite submodule is properly set up +if [ -d "jsonschema/oas3/tests/testsuite/tests/draft2020-12" ] && [ -n "$(ls -A jsonschema/oas3/tests/testsuite/tests/draft2020-12 2>/dev/null)" ]; then + echo "โœ… JSON Schema Test Suite submodule successfully initialized!" + echo " Location: jsonschema/oas3/tests/testsuite" + echo " Test files: $(find jsonschema/oas3/tests/testsuite/tests/draft2020-12 -name "*.json" | wc -l) JSON test files available" +else + echo "โš ๏ธ Warning: JSON Schema Test Suite submodule may not be fully initialized" + echo " Expected location: jsonschema/oas3/tests/testsuite/tests/draft2020-12" +fi + +echo "" +echo "๐Ÿงช You can now run 'mise run test' to execute all tests including JSON Schema Test Suite tests." \ No newline at end of file diff --git a/mise-tasks/test b/mise-tasks/test index df9daf2..8da773f 100755 --- a/mise-tasks/test +++ b/mise-tasks/test @@ -1,6 +1,13 @@ #!/usr/bin/env bash set -euo pipefail +# Check if JSON Schema Test Suite submodule is initialized +if [ ! -d "jsonschema/oas3/tests/testsuite/tests/draft2020-12" ] || [ -z "$(ls -A jsonschema/oas3/tests/testsuite/tests/draft2020-12 2>/dev/null)" ]; then + echo "โš ๏ธ JSON Schema Test Suite submodule not initialized." + echo " Some tests will be skipped. Run 'mise run setup-submodules' to enable all tests." + echo "" +fi + echo "๐Ÿงช Running tests with gotestsum..." gotestsum --format testname -- -race ./... echo "โœ… All tests passed!" \ No newline at end of file diff --git a/references/reference.go b/references/reference.go index 805646f..9fde75a 100644 --- a/references/reference.go +++ b/references/reference.go @@ -31,7 +31,16 @@ func (r Reference) GetJSONPointer() jsonpointer.JSONPointer { if len(parts) < 2 { return "" } - return jsonpointer.JSONPointer(strings.TrimSpace(parts[1])) + + pointer := strings.TrimSpace(parts[1]) + + // URL decode the JSON pointer to handle percent-encoded characters + // like %25 (which represents %) + if decoded, err := url.QueryUnescape(pointer); err == nil { + pointer = decoded + } + + return jsonpointer.JSONPointer(pointer) } func (r Reference) Validate() error { diff --git a/values/eithervalue.go b/values/eithervalue.go index 68e6f95..809bb8e 100644 --- a/values/eithervalue.go +++ b/values/eithervalue.go @@ -92,7 +92,7 @@ func (e *EitherValue[L, LCore, R, RCore]) RightValue() R { return *e.Right } -func (e *EitherValue[L, LCore, R, RCore]) Populate(source any) error { +func (e *EitherValue[L, LCore, R, RCore]) PopulateWithParent(source any, parent any) error { var ec *core.EitherValue[LCore, RCore] switch v := source.(type) { case *core.EitherValue[LCore, RCore]: @@ -107,7 +107,7 @@ func (e *EitherValue[L, LCore, R, RCore]) Populate(source any) error { e.SetCoreAny(ec) if ec.IsLeft { - if err := marshaller.Populate(ec.Left, &e.Left); err != nil { + if err := marshaller.PopulateWithParent(ec.Left, &e.Left, parent); err != nil { return fmt.Errorf("failed to populate left: %w", err) } diff --git a/yml/yml.go b/yml/yml.go index b4c0a3f..ee1962f 100644 --- a/yml/yml.go +++ b/yml/yml.go @@ -38,6 +38,7 @@ func CreateOrUpdateScalarNode(ctx context.Context, value any, valueNode *yaml.No if resolvedValueNode != nil { resolvedValueNode.Value = convNode.Value + resolvedValueNode.Tag = convNode.Tag // Also update the tag to match the new value type return valueNode } From d35e2ff1e921d1ce106e952f2a5d7339c0cb8d66 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Thu, 21 Aug 2025 17:58:42 +1000 Subject: [PATCH 2/4] fix --- go.mod | 20 +++++++++------- go.sum | 72 +++++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index a59e4a6..e36a209 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/speakeasy-api/jsonpath v0.6.2 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.38.0 github.com/vmware-labs/yaml-jsonpath v0.3.2 golang.org/x/sync v0.16.0 golang.org/x/text v0.28.0 @@ -31,14 +32,13 @@ require ( github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -57,15 +57,19 @@ require ( github.com/shirou/gopsutil/v4 v4.25.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/testcontainers/testcontainers-go v0.38.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/sys v0.32.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index ab80c7e..8c34bcc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -17,7 +19,8 @@ github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7np github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -40,8 +43,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -50,8 +53,12 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -60,8 +67,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -76,6 +84,8 @@ github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -102,6 +112,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= @@ -118,6 +130,8 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -139,33 +153,40 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -176,12 +197,16 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -190,9 +215,18 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8= +google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -205,3 +239,5 @@ gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= From dc9cf9e08b624133131979c3cf1aa06188a16b3b Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Thu, 21 Aug 2025 18:11:45 +1000 Subject: [PATCH 3/4] fix: resolve test failures and race conditions - Fix thread-safe coverage tracking using atomic operations and sync.WaitGroup - Remove conflicting reference validation test for empty tokens - Ensure RFC 6901 compliance for JSON pointer empty tokens --- jsonschema/oas3/tests/testsuite_test.go | 52 +++++++++++++++++++++---- references/reference_test.go | 5 --- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/jsonschema/oas3/tests/testsuite_test.go b/jsonschema/oas3/tests/testsuite_test.go index f8ee816..a79a7e5 100644 --- a/jsonschema/oas3/tests/testsuite_test.go +++ b/jsonschema/oas3/tests/testsuite_test.go @@ -10,6 +10,8 @@ import ( "os" "path/filepath" "strings" + "sync" + "sync/atomic" "testing" "github.com/speakeasy-api/openapi/jsonschema/oas3" @@ -112,6 +114,37 @@ const testSuiteDir = "testsuite/tests/draft2020-12" // Global variable to hold the remote server instance var remoteServer *RemoteServer +// Thread-safe coverage tracking +type CoverageTracker struct { + totalFiles int64 + skippedFiles int64 + totalCases int64 + skippedCases int64 + passedCases int64 +} + +func (c *CoverageTracker) AddFile() { + atomic.AddInt64(&c.totalFiles, 1) +} + +func (c *CoverageTracker) AddSkippedFile() { + atomic.AddInt64(&c.skippedFiles, 1) +} + +func (c *CoverageTracker) AddCases(total, skipped, passed int64) { + atomic.AddInt64(&c.totalCases, total) + atomic.AddInt64(&c.skippedCases, skipped) + atomic.AddInt64(&c.passedCases, passed) +} + +func (c *CoverageTracker) GetStats() (int, int, int, int, int) { + return int(atomic.LoadInt64(&c.totalFiles)), + int(atomic.LoadInt64(&c.skippedFiles)), + int(atomic.LoadInt64(&c.totalCases)), + int(atomic.LoadInt64(&c.skippedCases)), + int(atomic.LoadInt64(&c.passedCases)) +} + func TestMain(m *testing.M) { // Check if the git submodule is initialized if !isSubmoduleInitialized(testSuiteDir) { @@ -148,32 +181,35 @@ func TestJSONSchemaTestSuite_RoundTrip(t *testing.T) { // Get all test files testFiles := getAllTestFiles(t, testSuiteDir) - // Track coverage statistics - var totalFiles, skippedFiles, totalCases, skippedCases, passedCases int + // Thread-safe coverage tracking + tracker := &CoverageTracker{} + var wg sync.WaitGroup for _, testFile := range testFiles { - totalFiles++ + tracker.AddFile() // Check if this file is blacklisted if reason, isBlacklisted := blacklistedFiles[testFile]; isBlacklisted { - skippedFiles++ + tracker.AddSkippedFile() t.Run(testFile, func(t *testing.T) { t.Skipf("Skipping blacklisted file: %s", reason) }) continue } + wg.Add(1) t.Run(testFile, func(t *testing.T) { + defer wg.Done() t.Parallel() fileCases, fileSkipped, filePassed := runRoundTripTestFile(t, filepath.Join(testSuiteDir, testFile)) - totalCases += fileCases - skippedCases += fileSkipped - passedCases += filePassed + tracker.AddCases(int64(fileCases), int64(fileSkipped), int64(filePassed)) }) } - // Print coverage summary + // Print coverage summary after all tests complete t.Cleanup(func() { + wg.Wait() // Wait for all parallel tests to complete + totalFiles, skippedFiles, totalCases, skippedCases, passedCases := tracker.GetStats() printCoverageSummary(t, totalFiles, skippedFiles, totalCases, skippedCases, passedCases) }) } diff --git a/references/reference_test.go b/references/reference_test.go index cb0650e..3407cf0 100644 --- a/references/reference_test.go +++ b/references/reference_test.go @@ -103,11 +103,6 @@ func TestReference_Validate_Error(t *testing.T) { ref: "https://example .com/api.yaml#/User", expectError: "invalid reference URI", }, - { - name: "invalid JSON pointer - empty token after slash", - ref: "#/components//User", - expectError: "invalid reference JSON pointer", - }, } for _, tt := range tests { From 271d23790d00003c116e9fb70498d339c5f57c04 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Thu, 21 Aug 2025 18:21:28 +1000 Subject: [PATCH 4/4] ci: exclude JSON Schema Test Suite from Windows CI runs The JSON Schema Test Suite tests use testcontainers which require Docker. Since Docker is not available on Windows GitHub Actions runners, we exclude these tests from the Windows test run to prevent failures. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e99047d..9066969 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -90,7 +90,7 @@ jobs: if: matrix.os == 'windows-latest' env: ARAZZO_CACHE_DIR: ${{ runner.temp }} - run: gotestsum --format testname -- -race ./... + run: gotestsum --format testname -- -race $(go list ./... | grep -v 'jsonschema/oas3/tests') - name: Calculate coverage if: matrix.os == 'ubuntu-latest'