Repository navigation
feat: Go, Rust, Python, Java/Kotlin and Swift ports with shared test vectors - #3
Merged
Merged
Conversation
test-vectors/generate.mjs writes vectors.json from the TypeScript implementation: 2,859 hashing cases (ASCII, unicode, long ids, fractional and boundary splits, legacy hashing, custom variant names), 4,919 bucket mapping cases for crafted 64-bit values, and 26 error cases. bucket.ts now exposes bucketFromTop so the float conversion can be tested directly. A jest test checks the TypeScript implementation against the vectors so they cannot drift.
Module github.com/pranav2012/abtestify/go with GetBucket(Params) (Result, error). Nil pointer fields take the defaults. Tests run every shared vector, and pass on arm64 and amd64.
Keeps git, eslint and prettier away from Rust, Python, Gradle and SwiftPM build directories and the port sources.
Crate abtestify with get_bucket(&Params) -> Result<Assignment, Error>, a Params builder with Default, rustdoc and crates.io metadata. Zero dependencies: it carries a small SHA-256. Unit tests run every shared vector.
Package abtestify (hatchling, Python 3.9+, typed with py.typed) with get_ab_experiment_bucket(experiment_name, unique_id, *, ...) returning a frozen Assignment dataclass. Uses hashlib, no dependencies. unittest tests, also run by pytest, cover every shared vector.
Gradle (Kotlin DSL) project for io.github.pranav2012:abtestify, Java 8 bytecode, no runtime dependencies. ABTestify.getBucket(Params) with a builder, an immutable Assignment and InvalidParamsException (an IllegalArgumentException). Unsigned 64-bit to double conversion is proven against BigInteger.doubleValue and the bucket vectors. Publishing goes to Maven Central through the Central Portal with the com.vanniktech.maven.publish plugin and in-memory GPG signing.
Package.swift sits at the repository root, as SwiftPM requires, and points at swift/Sources/ABTestify and swift/Tests/ABTestifyTests. ABTestify.bucket(...) throws -> Assignment. A small SHA-256 keeps it dependency-free and buildable on Linux without CryptoKit. Tests run every shared vector.
README covers install and usage for npm, Go, Rust, Python, Java/Kotlin and Swift, the cross-language guarantee and how the shared test vectors enforce it. RELEASING.md has the owner's one-time registry setup and how releases flow.
pr-verify runs TypeScript, Go (1.18, stable, 386), Rust (stable and MSRV 1.60), Python 3.9 to 3.13, Java and Swift (Linux and macOS) against the shared vectors, and fails if vectors.json is stale. release.yml keeps the npm job and its version logic, then publishes the same version to crates.io, PyPI (trusted publishing) and Maven Central and pushes the go/v tag. Each registry job skips with a warning until its credentials or the PYPI_ENABLED variable exist.
It reads each token hidden and pipes it to gh secret set, creates the pypi environment and PYPI_ENABLED, and can generate and publish the GPG key.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports abtestify to Go, Rust, Python, Java (which also serves Kotlin) and Swift. Every port produces byte-for-byte the same assignments as the TypeScript implementation, and one release publishes all of them with the same version. The TypeScript package stays where it is; the npm tarball is unchanged (still only
dist).go/GetBucket(Params) (Result, error). Optional fields are pointers (abtestify.Float64,abtestify.String); returns*ValidationError{Field}go/vX.Y.Zrust/get_bucket(&Params) -> Result<Assignment, Error>, with aParams::new(..)builder andDefault. Zero deps (own SHA-256), MSRV 1.60python/get_ab_experiment_bucket(experiment_name, unique_id, *, treatment_split=50, ...) -> Assignment(frozen dataclass). Uses hashlib, typed withpy.typed, 3.9+java/ABTestify.getBucket(Params)/getBucket(name, id),Params.builder(..), an immutableAssignment, andInvalidParamsException extends IllegalArgumentException. Java 8 bytecodeio.github.pranav2012:abtestifyPackage.swift(root) +swift/ABTestify.bucket(experimentName:uniqueId:...) throws -> Assignment. Zero deps, no CryptoKit, builds on Linuxx.y.zrelease tagsHow parity is verified
test-vectors/generate.mjswritestest-vectors/vectors.jsonfrom the TypeScript implementation. Assignments come from the builtdist/index.js; the internalbucketFromTop/bucketOffromsrc/bucket.tsare compiled with esbuild. The generator uses a seeded PRNG, so its output is deterministic.legacyHashingboth ways, and custom variant names (including the empty string).top64Hexto bucket): 0, 1, 2^53±1, 2^63±1, 2^64−1, ties at every exponent from 2^53 to 2^63 (e.g. 2^54+2, 2^63+1536), and values at and ±1/±2/±2048 around the exact float boundary of ~400 buckets, plus the naive ceil(k·2^64/10000) boundaries. I checked that a truncating conversion fails 1,658 of these, ties-away fails 209, and exact integer math fails 1,667.bucket.tsnow exposesbucketFromTop(a pure refactor). A new jest test checks the TypeScript implementation against the vectors, and CI regenerates the file and fails if it differs from the committed copy. Java's unsigned conversion uses((x >>> 1) | (x & 1)) * 2.0and is also checked againstBigInteger.doubleValue()for every bucket case.Local test results (macOS arm64)
yarn testpasses: 52 tests, including the vector suite. Lint, prettier and type-check are clean.go test ./...passes on arm64, and on amd64 under Rosetta. 386 runs in CI only.cargo testpasses (5 unit tests + 1 doctest). Clippy and fmt are clean,cargo packagesucceeds, and the library builds on Rust 1.60.mypy --strictis clean and the wheel/sdist build. 3.9 to 3.12 run in CI only../gradlew testpasses on JDK 17 (bytecode major 52), andpublishToMavenLocalproduces the jar, sources, javadoc and POM. Signing is not tested locally because there's no gpg here.swift testpasses on macOS with Swift 6.3. Linux (swift:6.0container) runs in CI only.CI and release
pr-verify.ymlruns these jobs in parallel: typescript (with the vectors-fresh check), go (1.18 and stable, plus 386), rust and rust-msrv, python 3.9 to 3.13 plus a package check, java, swift-linux and swift-macos.release.ymlkeeps the npm job and its version logic unchanged, apart from bumping checkout and setup-node to v4. After the GitHub release (whose tag is also the Swift release), these jobs run with the samenew_version:tag-gopushesgo/v<version>.publish-cratespublishes to crates.io.publish-pypipublishes through trusted publishing in environmentpypi.publish-mavenpublishes to Maven Central through the vanniktech plugin with in-memory signing.::warning::until its credentials exist. PyPI is gated on the variablevars.PYPI_ENABLED == 'true'.Owner checklist before merging (details in RELEASING.md)
CARGO_REGISTRY_TOKEN.abtestify, ownerpranav2012, repoabtestify, workflowrelease.yml, environmentpypi), create the GitHub environmentpypi, and set the repo variablePYPI_ENABLED=true.io.github.pranav2012), generate a user token, and add it asMAVEN_CENTRAL_USERNAME/MAVEN_CENTRAL_PASSWORD.SIGNING_KEYand the passphrase asSIGNING_PASSWORD.package.jsonis 0.2.0 and the latest release is 0.2.1, so merging as-is releases 0.2.2 everywhere. Setpackage.jsonto0.3.0first if you want a minor release for the new ports.