Repository navigation
FFL-3568: Run shared precomputed response fixtures #1534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
leoromanovsky
wants to merge
8
commits into
develop
Choose a base branch
from
leo.romanovsky/precompute-fixture-fanout
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+16,080
−11
Draft
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5b3ba68
FFL-3568: Validate shared precomputed responses and reject encodings
leoromanovsky df25fb8
FFL-3568: Import upstream serial-ID response coverage
leoromanovsky f0f560f
FFL-3568: Narrow decoded serial ID before validation
leoromanovsky f9ffb76
FFL-3568: Apply lossless numeric contract and shared boundaries
leoromanovsky 2a7858b
FFL-3568: Pin checkout and satisfy fixture importer lint
leoromanovsky 508b422
FFL-3568: Sync generated obfuscation coverage
leoromanovsky 8898519
FFL-3568: Exclude Flags test sources from the npm package
leoromanovsky 7b4443b
FFL-3568: Refresh integral floating-value fixtures
leoromanovsky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| issuer: https://token.actions.githubusercontent.com | ||
| subject: repo:DataDog/dd-sdk-reactnative:ref:refs/heads/develop | ||
| claim_pattern: | ||
| event_name: "(schedule|workflow_dispatch)" | ||
| workflow_ref: 'DataDog/dd-sdk-reactnative/\.github/workflows/precomputed-fixtures\.yml@refs/heads/develop' | ||
| repository: "DataDog/dd-sdk-reactnative" | ||
| permissions: | ||
| contents: write | ||
| pull_requests: write |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| name: Precomputed flag fixtures | ||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'ci/*precomputed*' | ||
| - 'packages/core/src/flags/__tests__/fixtures/precomputed/**' | ||
| - '.github/workflows/precomputed-fixtures.yml' | ||
| schedule: | ||
| - cron: '23 8 * * 1' | ||
| workflow_dispatch: | ||
| permissions: | ||
| contents: read | ||
| concurrency: | ||
| group: precomputed-fixtures-${{ github.event_name }}-${{ github.ref }} | ||
| cancel-in-progress: false | ||
| jobs: | ||
| verify: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Verify the local snapshot | ||
| run: python3 ci/sync-precomputed-fixtures.py --verify | ||
| - name: Compare with the recorded upstream commit | ||
| run: python3 ci/sync-precomputed-fixtures.py --check | ||
| update: | ||
| if: github.event_name != 'pull_request' && github.ref == 'refs/heads/develop' | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| steps: | ||
| - uses: DataDog/dd-octo-sts-action@c85802a58e202b7e764b5441e4378c76043c8108 # v1.0.5 | ||
| id: token | ||
| with: | ||
| scope: DataDog/dd-sdk-reactnative | ||
| policy: self.precomputed-fixtures | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Import upstream fixtures | ||
| id: sync | ||
| run: python3 ci/sync-precomputed-fixtures.py --ref main | ||
| - name: Prepare a draft update | ||
| id: prepare | ||
| if: steps.sync.outputs.changed == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ steps.token.outputs.token }} | ||
| SOURCE_COMMIT: ${{ steps.sync.outputs.source_commit }} | ||
| run: | | ||
| existing=$(gh pr list --state open --search '"Update precomputed flag fixtures" in:title' --json number --jq 'length') | ||
| if [ "$existing" != 0 ]; then | ||
| echo 'An update PR is already open. Merge or close it before the next update.' | ||
| exit 0 | ||
| fi | ||
| branch="automation/precomputed-fixtures-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | ||
| git add -- 'packages/core/src/flags/__tests__/fixtures/precomputed' | ||
| echo "branch=$branch" >> "$GITHUB_OUTPUT" | ||
| echo 'ready=true' >> "$GITHUB_OUTPUT" | ||
| python3 - <<'BODY' | ||
| import os | ||
| from pathlib import Path | ||
| template = Path('ci/precomputed-fixtures-pr.md').read_text() | ||
| Path('/tmp/precomputed-pr.md').write_text(template.replace('SOURCE_COMMIT', os.environ['SOURCE_COMMIT'])) | ||
| BODY | ||
| - name: Create a signed fixture commit | ||
| if: steps.prepare.outputs.ready == 'true' | ||
| uses: DataDog/commit-headless@d6afabfd690ea1dbdd7d368acbefd71304928699 # action/v3.4.1 | ||
| with: | ||
| command: commit | ||
| token: ${{ steps.token.outputs.token }} | ||
| branch: ${{ steps.prepare.outputs.branch }} | ||
| head-sha: ${{ github.sha }} | ||
| create-branch: true | ||
| force: false | ||
| message: 'FFL-3568: Update precomputed flag fixtures' | ||
| - name: Open the draft pull request | ||
| if: steps.prepare.outputs.ready == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ steps.token.outputs.token }} | ||
| UPDATE_BRANCH: ${{ steps.prepare.outputs.branch }} | ||
| run: | | ||
| gh pr create --draft --base develop --head "$UPDATE_BRANCH" --title 'FFL-3568: Update precomputed flag fixtures' --body-file /tmp/precomputed-pr.md |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ### What does this PR do? | ||
|
|
||
| Refresh the shared precomputed response fixtures to `SOURCE_COMMIT`. | ||
|
|
||
| ### Motivation | ||
|
|
||
| Keep mobile response tests aligned with the canonical JSON contract. Ref: FFL-3568. | ||
|
|
||
| ### Additional Notes | ||
|
|
||
| The existing fixture loop runs new cases in normal SDK CI. Review those results before merge. | ||
|
|
||
| ### Review checklist (to be filled by reviewers) | ||
|
|
||
| - [ ] Feature or bugfix MUST have appropriate tests | ||
| - [ ] Make sure you discussed the feature or bugfix with the maintaining team in an Issue | ||
| - [ ] Make sure each commit and the PR mention the Issue number (cf the [CONTRIBUTING](CONTRIBUTING.md) doc) | ||
| - [ ] If this PR is auto-generated, please make sure also to manually update the code related to the change |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "destination": "packages/core/src/flags/__tests__/fixtures/precomputed" | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| #!/usr/bin/env python3 | ||
| # Unless explicitly stated otherwise, all files in this repository are licensed under the | ||
| # Apache License Version 2.0. This product includes software developed at Datadog | ||
| # (https://www.datadoghq.com/). Copyright 2025-Present Datadog, Inc. | ||
| """Import a reproducible precompute test snapshot; never edit imported JSON.""" | ||
| import argparse | ||
| import hashlib | ||
| import json | ||
| import os | ||
| import re | ||
| import subprocess | ||
| import tempfile | ||
| import uuid | ||
| from pathlib import Path | ||
|
|
||
| REPOSITORY = 'https://github.com/DataDog/ffe-system-test-data.git' | ||
| PREFIX = 'precomputed-assignments/' | ||
| FILES = { | ||
| 'schemas/precomputed-assignment.schema.json': 'schema.json', | ||
| 'precomputed-assignments/README.md': 'README.md', | ||
| 'LICENSE': 'LICENSE', | ||
| 'NOTICE': 'NOTICE', | ||
| } | ||
|
|
||
|
|
||
| def git(source, *args): | ||
| return subprocess.check_output(['git', '-C', str(source), *args]) | ||
|
|
||
|
|
||
| def snapshot(source, ref): | ||
| if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', ref) or '..' in ref: | ||
| raise ValueError('Invalid fixture revision') | ||
| sha = git(source, 'rev-parse', '--verify', ref + '^{commit}').decode().strip() | ||
| if not re.fullmatch(r'[0-9a-f]{40}', sha): | ||
| raise ValueError('Expected a full source commit SHA') | ||
| result = {} | ||
| for entry in git(source, 'ls-tree', '-rz', '--full-tree', sha).split(b'\0'): | ||
| if not entry: | ||
| continue | ||
| metadata, raw_path = entry.split(b'\t', 1) | ||
| path = raw_path.decode() | ||
| target = FILES.get(path) | ||
| if path.startswith(PREFIX + 'cases/') and path.endswith('.json'): | ||
| target = path[len(PREFIX):] | ||
| if target is None: | ||
| continue | ||
| mode, kind, blob = metadata.decode().split() | ||
| if mode != '100644' or kind != 'blob': | ||
| raise ValueError('Fixture entries must be ordinary files: ' + path) | ||
| result[target] = git(source, 'cat-file', 'blob', blob) | ||
| cases = sorted(p for p in result if p.startswith('cases/')) | ||
| if not cases or not all(p in result for p in FILES.values()): | ||
| raise ValueError('Missing or empty precomputed fixture corpus') | ||
| for path in cases: | ||
| data = json.loads(result[path]) | ||
| if not isinstance(data, dict) or data.get('name') != Path(path).stem: | ||
| raise ValueError('Invalid fixture identity: ' + path) | ||
| if not isinstance(data.get('evaluations'), list): | ||
| raise TypeError('Missing evaluations: ' + path) | ||
| result['index.json'] = (json.dumps(cases, indent=2) + '\n').encode() | ||
| result['SOURCE.json'] = (json.dumps({ | ||
| 'repository': REPOSITORY, | ||
| 'commit': sha, | ||
| 'files': {p: hashlib.sha256(v).hexdigest() for p, v in sorted(result.items())}, | ||
| }, indent=2) + '\n').encode() | ||
| return result | ||
|
|
||
|
|
||
| def read_snapshot(destination): | ||
| if not destination.exists(): | ||
| return {} | ||
| result = {} | ||
| for path in destination.rglob('*'): | ||
| if path.is_symlink(): | ||
| raise ValueError('Snapshot must not contain symbolic links') | ||
| if path.is_file() and path.suffix != '.meta': | ||
| result[path.relative_to(destination).as_posix()] = path.read_bytes() | ||
| return result | ||
|
|
||
|
|
||
| def verify(destination): | ||
| content = read_snapshot(destination) | ||
| metadata = json.loads(content.pop('SOURCE.json')) | ||
| if metadata['repository'] != REPOSITORY or not re.fullmatch(r'[0-9a-f]{40}', metadata['commit']): | ||
| raise ValueError('Invalid source provenance') | ||
| hashes = {p: hashlib.sha256(v).hexdigest() for p, v in sorted(content.items())} | ||
| if hashes != metadata['files']: | ||
| raise ValueError('Snapshot content differs from its recorded checksums') | ||
| cases = sorted(p for p in content if p.startswith('cases/') and p.endswith('.json')) | ||
| if not cases or json.loads(content['index.json']) != cases: | ||
| raise ValueError('Missing, empty, or stale fixture index') | ||
| print(f"Verified {len(cases)} fixtures at {metadata['commit']}") | ||
|
|
||
|
|
||
| def unity_metadata(destination): | ||
| for asset in sorted(destination.rglob('*')): | ||
| if asset.suffix == '.meta': | ||
| continue | ||
| meta = asset.with_name(asset.name + '.meta') | ||
| if not meta.exists(): | ||
| guid = uuid.uuid5(uuid.NAMESPACE_URL, 'ffe-precomputed/' + asset.relative_to(destination).as_posix()).hex | ||
| folder = 'folderAsset: yes\n' if asset.is_dir() else '' | ||
| importer = 'DefaultImporter' if asset.is_dir() or asset.suffix != '.json' else 'TextScriptImporter' | ||
| meta.write_text(f'fileFormatVersion: 2\nguid: {guid}\n{folder}{importer}:\n externalObjects: {{}}\n userData:\n assetBundleName:\n assetBundleVariant:\n') | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument('--ref', default='main') | ||
| parser.add_argument('--source', type=Path, help='Read a local Git checkout at the requested committed revision') | ||
| parser.add_argument('--verify', action='store_true', help='Verify local checksums and discovery index without network access') | ||
| parser.add_argument('--check', action='store_true', help='Compare with the recorded upstream SHA; do not write') | ||
| args = parser.parse_args() | ||
| root = Path(__file__).resolve().parent.parent | ||
| config = json.loads((root / 'ci/precomputed-fixtures.json').read_text()) | ||
| relative = Path(config['destination']) | ||
| if relative.is_absolute() or '..' in relative.parts: | ||
| raise ValueError('Destination must be inside the repository') | ||
| destination = root / relative | ||
| if not destination.resolve().is_relative_to(root): | ||
| raise ValueError('Destination escapes the repository') | ||
| if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/-]*', args.ref) or '..' in args.ref: | ||
| raise ValueError('Invalid fixture revision') | ||
| if args.verify: | ||
| verify(destination) | ||
| return | ||
| before = read_snapshot(destination) | ||
| ref = json.loads(before['SOURCE.json'])['commit'] if args.check else args.ref | ||
| with tempfile.TemporaryDirectory(prefix='ffe-precompute-') as temporary: | ||
| source = args.source | ||
| if source is None: | ||
| source = Path(temporary) / 'source' | ||
| subprocess.run(['git', 'init', '-q', str(source)], check=True) | ||
| subprocess.run(['git', '-C', str(source), 'fetch', '-q', '--depth=1', REPOSITORY, ref], check=True) | ||
| ref = 'FETCH_HEAD' | ||
| after = snapshot(source, ref) | ||
| changed = {p:v for p,v in before.items() if p != 'SOURCE.json'} != {p:v for p,v in after.items() if p != 'SOURCE.json'} | ||
| if args.check: | ||
| if before != after: | ||
| raise ValueError('Snapshot differs from its recorded source commit') | ||
| elif changed: | ||
| destination.parent.mkdir(parents=True, exist_ok=True) | ||
| with tempfile.TemporaryDirectory(prefix='.precompute-', dir=destination.parent) as staging: | ||
| staged = Path(staging) / 'snapshot' | ||
| staged.mkdir() | ||
| for path, content in after.items(): | ||
| output = staged / path | ||
| output.parent.mkdir(parents=True, exist_ok=True) | ||
| output.write_bytes(content) | ||
| # Preserve Unity's existing asset identities when refreshing a snapshot. | ||
| if destination.exists(): | ||
| for meta in destination.rglob('*.meta'): | ||
| rel = meta.relative_to(destination) | ||
| asset = staged / str(rel)[:-5] | ||
| if asset.exists(): | ||
| (staged / rel).write_bytes(meta.read_bytes()) | ||
| backup = Path(staging) / 'previous' | ||
| destination.rename(backup) | ||
| try: | ||
| staged.rename(destination) | ||
| except BaseException: | ||
| backup.rename(destination) | ||
| raise | ||
| else: | ||
| staged.rename(destination) | ||
| if config.get('unityMetadata') and not args.check: | ||
| unity_metadata(destination) | ||
| selected = before if args.check or not changed else after | ||
| metadata = json.loads(selected['SOURCE.json']) | ||
| count = len(json.loads(selected['index.json'])) | ||
| print(f"FFE precomputed fixtures: {metadata['commit']}; {count} files; changed={str(changed).lower()}") | ||
| if os.environ.get('GITHUB_OUTPUT'): | ||
| with open(os.environ['GITHUB_OUTPUT'], 'a') as output: | ||
| output.write(f"changed={str(changed).lower()}\nsource_commit={metadata['commit']}\nfixture_count={count}\n") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() |
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Comment from Claude, working with Aaron:
[P2] The Number getter returns rounded values.
This line says Double getters reject integer values that lose precision. The offline path returns rounded values with
TARGETING_MATCH.A local probe used
configurationFromString,setConfiguration, andgetNumberDetails. All three numeric labels returned the same results:9007199254740993→90071992547409929223372036854775807→92233720368547760009007199254740993.5→9007199254740994This behavior predates this PR.
JSON.parserounds the input before decoding. The shared contract requires the default andTYPE_MISMATCHfor inexact conversions.The three
numeric-boundaries-*fixtures exclude React Native at the case level. Their evaluation filters permit 27 Number reads. A local probe bypassed the case filters. Eighteen results matched; nine differed because the getter accepted rounded values.Could we detect precision loss before parsing, or document a React Native exception in the shared contract and correct this section? Please also narrow the fixture filters so the other 18 reads run.