Skip to content

Commit fbe31ff

Browse files
authored
Merge pull request #195 from firedancer-io/cmoyes/protocolfix
Add support for new FCNG protocol changes, friendlier error messages
2 parents 8346f89 + 42c68a5 commit fbe31ff

File tree

5 files changed

+237
-116
lines changed

5 files changed

+237
-116
lines changed

commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ $ solana-conformance debug-mismatches [OPTIONS]
166166
* `-l, --section-limit INTEGER`: Limit number of fixture per section [default: 0]
167167
* `--use-ng`: Use fuzz NG CLI (fuzz list/download repro) instead of API scraping [default: True]
168168
* `-d, --debug-mode`: Enables debug mode, which spawns a single child process for easier debugging
169-
* `--all-artifacts`: Download all artifacts per repro (default: only latest)
169+
* `--all-artifacts`: (Deprecated, all artifacts are now always downloaded)
170170
* `--help`: Show this message and exit.
171171

172172
## `solana-conformance decode-protobufs`
@@ -268,7 +268,7 @@ $ solana-conformance download-fixtures [OPTIONS]
268268
* `-p, --num-processes INTEGER`: Number of parallel download processes [default: 4]
269269
* `--use-ng`: Use fuzz NG CLI (fuzz list/download repro) instead of API scraping [default: True]
270270
* `--interactive / --no-interactive`: Prompt for authentication if needed [default: interactive]
271-
* `--all-artifacts`: Download all artifacts per repro (default: only latest)
271+
* `--all-artifacts`: (Deprecated, all artifacts are now always downloaded)
272272
* `--help`: Show this message and exit.
273273

274274
## `solana-conformance exec-fixtures`

src/test_suite/fuzzcorp_api_client.py

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,60 @@ class LineageRepro:
3232
created_at: datetime
3333
count: int
3434
all_verified: bool
35+
# Optional extra metadata available on newer APIs
36+
drivers: Optional[List[str]] = None
37+
config_indices: Optional[List[int]] = None
3538

3639
@classmethod
3740
def from_dict(cls, data: Dict[str, Any]) -> "LineageRepro":
38-
created_at_str = data["CreatedAt"]
39-
# Handle both RFC3339 formats
40-
if created_at_str.endswith("Z"):
41-
created_at_str = created_at_str[:-1] + "+00:00"
41+
# Hash is required in all known schemas
42+
hash_value = data.get("Hash") or data.get("hash")
43+
if not hash_value:
44+
raise KeyError("Hash")
45+
46+
# CreatedAt is present in both schemas, normalize if available
47+
created_at_str = data.get("CreatedAt") or data.get("created_at")
48+
if created_at_str:
49+
# Handle both RFC3339 formats
50+
if created_at_str.endswith("Z"):
51+
created_at_str = created_at_str[:-1] + "+00:00"
52+
created_at = datetime.fromisoformat(created_at_str)
53+
else:
54+
# Fallback to a stable value so sorting still works
55+
created_at = datetime.min
56+
57+
# Count:
58+
# - prefer explicit Count from legacy schema
59+
# - otherwise derive from ConfigIndices (one count per config index)
60+
# - final fallback is a single repro
61+
if "Count" in data:
62+
count = int(data["Count"])
63+
else:
64+
cfg_indices = data.get("ConfigIndices") or data.get("config_indices") or []
65+
if isinstance(cfg_indices, list):
66+
count = max(len(cfg_indices), 1) if cfg_indices else 1
67+
else:
68+
# Unexpected shape: treat as a single repro
69+
count = 1
70+
71+
# AllVerified:
72+
# - legacy AllVerified flag if present
73+
# - otherwise fall back to the newer Verified flag
74+
if "AllVerified" in data:
75+
all_verified = bool(data["AllVerified"])
76+
else:
77+
all_verified = bool(data.get("Verified") or data.get("verified") or False)
78+
79+
drivers = data.get("Drivers") or data.get("drivers")
80+
cfg_indices_val = data.get("ConfigIndices") or data.get("config_indices")
4281

4382
return cls(
44-
hash=data["Hash"],
45-
created_at=datetime.fromisoformat(created_at_str),
46-
count=data["Count"],
47-
all_verified=data["AllVerified"],
83+
hash=hash_value,
84+
created_at=created_at,
85+
count=count,
86+
all_verified=all_verified,
87+
drivers=drivers,
88+
config_indices=cfg_indices_val,
4889
)
4990

5091

@@ -56,14 +97,23 @@ class ReproIndexResponse:
5697
@classmethod
5798
def from_dict(cls, data: Dict[str, Any]) -> "ReproIndexResponse":
5899
lineages = {}
59-
lineages_data = data.get("Lineages") or {}
100+
# Support both legacy "Lineages" and potential future "lineages"
101+
lineages_data = data.get("Lineages") or data.get("lineages") or {}
60102
for lineage_name, repros in lineages_data.items():
61103
lineages[lineage_name] = [LineageRepro.from_dict(r) for r in (repros or [])]
62104

63-
return cls(
64-
bundle_id=data.get("BundleID", "00000000-0000-0000-0000-000000000000"),
65-
lineages=lineages,
66-
)
105+
# Bundle ID:
106+
# - legacy compat: top-level "BundleID"
107+
# - current NG API: "Bundle" object with "id" field
108+
bundle_id = data.get("BundleID")
109+
if not bundle_id:
110+
bundle = data.get("Bundle") or data.get("bundle")
111+
if isinstance(bundle, dict):
112+
bundle_id = bundle.get("id") or bundle.get("ID")
113+
if not bundle_id:
114+
bundle_id = data.get("bundle_id", "00000000-0000-0000-0000-000000000000")
115+
116+
return cls(bundle_id=bundle_id, lineages=lineages)
67117

68118

69119
@dataclass
@@ -78,14 +128,40 @@ class ReproMetadata:
78128

79129
@classmethod
80130
def from_dict(cls, data: Dict[str, Any]) -> "ReproMetadata":
131+
if "repros" in data and isinstance(data["repros"], dict):
132+
inner = data["repros"]
133+
else:
134+
inner = data
135+
136+
# Core identifiers
137+
hash_val = inner.get("hash") or inner.get("Hash")
138+
if not hash_val:
139+
raise KeyError("hash")
140+
141+
bundle_val = inner.get("bundle") or inner.get("Bundle")
142+
lineage_val = inner.get("lineage") or inner.get("Lineage")
143+
asset_val = inner.get("asset") or inner.get("Asset")
144+
145+
# Artifact hashes:
146+
artifact_hashes_val: List[str]
147+
raw_hashes = inner.get("artifact_hashes")
148+
if raw_hashes:
149+
artifact_hashes_val = list(raw_hashes)
150+
else:
151+
single_hash = inner.get("artifact_hash") or inner.get("ArtifactHash")
152+
if single_hash:
153+
artifact_hashes_val = [single_hash]
154+
else:
155+
artifact_hashes_val = []
156+
81157
return cls(
82-
hash=data["hash"],
83-
bundle=data["bundle"],
84-
lineage=data["lineage"],
85-
asset=data.get("asset"),
86-
artifact_hashes=data.get("artifact_hashes") or [],
87-
summary=data.get("summary") or "",
88-
flaky=data.get("flaky", False),
158+
hash=hash_val,
159+
bundle=str(bundle_val) if bundle_val is not None else "",
160+
lineage=lineage_val or "",
161+
asset=asset_val,
162+
artifact_hashes=artifact_hashes_val,
163+
summary=inner.get("summary") or "",
164+
flaky=bool(inner.get("flaky", False)),
89165
)
90166

91167

src/test_suite/multiprocessing_utils.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -684,30 +684,24 @@ def download_and_process(source):
684684
project=config.get_project(),
685685
)
686686

687+
# Require at least one artifact hash. If none are present, there is
688+
# nothing we can download or convert into fixtures...
687689
if not repro_metadata.artifact_hashes:
688690
return {
689691
"success": False,
690692
"repro": f"{section_name}/{crash_hash}",
691693
"message": "Failed to process: no artifacts found",
692694
}
693695

694-
# Determine which artifacts to download
695-
download_all = getattr(globals, "download_all_artifacts", False)
696-
if download_all:
697-
artifacts_to_download = repro_metadata.artifact_hashes
698-
print(
699-
f" [{section_name}/{crash_hash[:8]}] Downloading all {len(artifacts_to_download)} artifact(s)",
700-
file=sys.stderr,
701-
flush=True,
702-
)
703-
else:
704-
# Take only the first artifact hash
705-
artifacts_to_download = [repro_metadata.artifact_hashes[0]]
706-
print(
707-
f" [{section_name}/{crash_hash[:8]}] Using first artifact: {artifacts_to_download[0][:16]}",
708-
file=sys.stderr,
709-
flush=True,
710-
)
696+
# Determine which artifacts to download: Always process all available
697+
# artifacts for this repro. Any duplicates will be naturally deduped
698+
# later at the fixture level by hash.
699+
artifacts_to_download = repro_metadata.artifact_hashes
700+
print(
701+
f" [{section_name}/{crash_hash[:8]}] Downloading {len(artifacts_to_download)} artifact(s)",
702+
file=sys.stderr,
703+
flush=True,
704+
)
711705

712706
# Download and extract artifacts
713707
fix_count = 0
@@ -772,11 +766,7 @@ def download_and_process(source):
772766

773767
# Always return success if we processed artifacts (even if no new fixtures extracted)
774768
# Not extracting new fixtures just means they already exist or artifacts don't contain .fix files
775-
artifact_msg = (
776-
f"{len(artifacts_to_download)} artifact(s)"
777-
if len(artifacts_to_download) > 1
778-
else "latest artifact"
779-
)
769+
artifact_msg = f"{len(artifacts_to_download)} artifact(s)"
780770
return {
781771
"success": True,
782772
"repro": f"{section_name}/{crash_hash}",

0 commit comments

Comments
 (0)