Skip to content

Commit 664565d

Browse files
danceratopzfelix314159
authored andcommitted
feat(consume): add support for ethereum/{tests,legacytests} tarball urls (ethereum#1306)
* feat(consume): add support for ethereum/{tests,legacytests} tarball urls * docs: update changelog * fix(consume): don't extract to hard-coded fixtures directory Detect single directory within archive instead of a hard-coded "fixtures" directory. * fix(consume): fix tag matching when fetching releases * docs: add advisory to changelog about cache dir changes * fix(consume): revert incorrect "fix" from 39b0f54
1 parent 39eb2f6 commit 664565d

File tree

4 files changed

+94
-42
lines changed

4 files changed

+94
-42
lines changed

docs/CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,31 @@
22

33
Test fixtures for use by clients are available for each release on the [Github releases page](https://github.com/ethereum/execution-spec-tests/releases).
44

5-
**Key:** ✨ = New, 🐞 = Fixed, 🔀 = Changed.
5+
**Key:** ✨ = New, 🐞 = Fixed, 🔀 = Changed. 💥 = Breaking
66

77
## 🔜 [Unreleased]
88

9+
**Note**: Although not a breaking change, `consume` users should delete the cache directory (typically located at `~/.cache/ethereum-execution-spec-tests`) used to store downloaded fixture release tarballs. This release adds support for [ethereum/tests](https://github.com/ethereum/tests) and [ethereum/legacytests](https://github.com/ethereum/legacytests) fixture release downloads and the structure of the cache directory has been updated to accommodate this change.
10+
11+
To try this feature:
12+
13+
```shell
14+
consume direct --input=https://github.com/ethereum/tests/releases/download/v17.0/fixtures_blockchain_tests.tgz
15+
```
16+
17+
To determine the cache directory location, see the `--cache-folder` entry from the command:
18+
19+
```shell
20+
consume cache --help
21+
```
22+
923
### 💥 Breaking Change
1024

1125
### 🛠️ Framework
1226

1327
#### `consume`
1428

29+
- ✨ Add support for [ethereum/tests](https://github.com/ethereum/tests) and [ethereum/legacytests](https://github.com/ethereum/legacytests) release tarball download via URL to the `--input` flag of `consume` commands ([#1306](https://github.com/ethereum/execution-spec-tests/pull/1306)).
1530
- ✨ Add support for Nethermind's `nethtest` command to `consume direct` ([#1250](https://github.com/ethereum/execution-spec-tests/pull/1250)).
1631
- ✨ Allow filtering of test cases by fork via pytest marks (e.g., `-m "Cancun or Prague"`) ([#1304](https://github.com/ethereum/execution-spec-tests/pull/1304), [#1318](https://github.com/ethereum/execution-spec-tests/pull/1318)).
1732
- ✨ Allow filtering of test cases by fixture format via pytest marks (e.g., `-m blockchain_test`) ([#1314](https://github.com/ethereum/execution-spec-tests/pull/1314)).

src/pytest_plugins/consume/consume.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,58 @@ def default_html_report_file_path() -> str:
4343
return ".meta/report_consume.html"
4444

4545

46+
class FixtureDownloader:
47+
"""Handles downloading and extracting fixture archives."""
48+
49+
def __init__(self, url: str, base_directory: Path): # noqa: D107
50+
self.url = url
51+
self.base_directory = base_directory
52+
self.parsed_url = urlparse(url)
53+
self.org_repo = self.extract_github_repo()
54+
self.version = Path(self.parsed_url.path).parts[-2]
55+
self.archive_name = self.strip_archive_extension(Path(self.parsed_url.path).name)
56+
self.extract_to = base_directory / self.org_repo / self.version / self.archive_name
57+
58+
def download_and_extract(self) -> Tuple[bool, Path]:
59+
"""Download the URL and extract it locally if it hasn't already been downloaded."""
60+
if self.extract_to.exists():
61+
return True, self.detect_extracted_directory()
62+
63+
return False, self.fetch_and_extract()
64+
65+
def extract_github_repo(self) -> str:
66+
"""Extract <username>/<repo> from GitHub URLs, otherwise return 'other'."""
67+
parts = self.parsed_url.path.strip("/").split("/")
68+
return (
69+
f"{parts[0]}/{parts[1]}"
70+
if self.parsed_url.netloc == "github.com" and len(parts) >= 2
71+
else "other"
72+
)
73+
74+
@staticmethod
75+
def strip_archive_extension(filename: str) -> str:
76+
"""Remove .tar.gz or .tgz extensions from filename."""
77+
return filename.removesuffix(".tar.gz").removesuffix(".tgz")
78+
79+
def fetch_and_extract(self) -> Path:
80+
"""Download and extract an archive from the given URL."""
81+
self.extract_to.mkdir(parents=True, exist_ok=False)
82+
response = requests.get(self.url)
83+
response.raise_for_status()
84+
85+
with tarfile.open(fileobj=BytesIO(response.content), mode="r:gz") as tar:
86+
tar.extractall(path=self.extract_to)
87+
88+
return self.detect_extracted_directory()
89+
90+
def detect_extracted_directory(self) -> Path:
91+
"""
92+
Detect a single top-level dir within the extracted archive, otherwise return extract_to.
93+
""" # noqa: D200
94+
extracted_dirs = [d for d in self.extract_to.iterdir() if d.is_dir()]
95+
return extracted_dirs[0] if len(extracted_dirs) == 1 else self.extract_to
96+
97+
4698
@dataclass
4799
class FixturesSource:
48100
"""Represents the source of test fixtures."""
@@ -70,7 +122,8 @@ def from_input(cls, input_source: str) -> "FixturesSource":
70122
def from_url(cls, url: str) -> "FixturesSource":
71123
"""Create a fixture source from a direct URL."""
72124
release_page = get_release_page_url(url)
73-
was_cached, path = download_and_extract(url, CACHED_DOWNLOADS_DIRECTORY)
125+
downloader = FixtureDownloader(url, CACHED_DOWNLOADS_DIRECTORY)
126+
was_cached, path = downloader.download_and_extract()
74127
return cls(
75128
input_option=url,
76129
path=path,
@@ -85,7 +138,8 @@ def from_release_spec(cls, spec: str) -> "FixturesSource":
85138
"""Create a fixture source from a release spec (e.g., develop@latest)."""
86139
url = get_release_url(spec)
87140
release_page = get_release_page_url(url)
88-
was_cached, path = download_and_extract(url, CACHED_DOWNLOADS_DIRECTORY)
141+
downloader = FixtureDownloader(url, CACHED_DOWNLOADS_DIRECTORY)
142+
was_cached, path = downloader.download_and_extract()
89143
return cls(
90144
input_option=spec,
91145
path=path,
@@ -111,25 +165,6 @@ def is_url(string: str) -> bool:
111165
return all([result.scheme, result.netloc])
112166

113167

114-
def download_and_extract(url: str, base_directory: Path) -> Tuple[bool, Path]:
115-
"""Download the URL and extract it locally if it hasn't already been downloaded."""
116-
parsed_url = urlparse(url)
117-
filename = Path(parsed_url.path).name
118-
version = Path(parsed_url.path).parts[-2]
119-
extract_to = base_directory / version / filename.removesuffix(".tar.gz")
120-
already_cached = extract_to.exists()
121-
if already_cached:
122-
return already_cached, extract_to / "fixtures"
123-
124-
extract_to.mkdir(parents=True, exist_ok=False)
125-
response = requests.get(url)
126-
response.raise_for_status()
127-
128-
with tarfile.open(fileobj=BytesIO(response.content), mode="r:gz") as tar:
129-
tar.extractall(path=extract_to)
130-
return already_cached, extract_to / "fixtures"
131-
132-
133168
class SimLimitBehavior:
134169
"""Represents options derived from the `--sim.limit` argument."""
135170

src/pytest_plugins/consume/releases.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import os
5+
import re
56
from dataclasses import dataclass
67
from datetime import datetime
78
from pathlib import Path
@@ -11,13 +12,12 @@
1112
import requests
1213
from pydantic import BaseModel, Field, RootModel
1314

14-
RELEASE_INFORMATION_URL = "https://api.github.com/repos/ethereum/execution-spec-tests/releases"
15-
16-
1715
CACHED_RELEASE_INFORMATION_FILE = (
1816
Path(platformdirs.user_cache_dir("ethereum-execution-spec-tests")) / "release_information.json"
1917
)
2018

19+
SUPPORTED_REPOS = ["ethereum/execution-spec-tests", "ethereum/tests", "ethereum/legacytests"]
20+
2121

2222
class NoSuchReleaseError(Exception):
2323
"""Raised when a release does not exist."""
@@ -154,19 +154,20 @@ def download_release_information(destination_file: Path | None) -> List[ReleaseI
154154
crucial for finding older version or latest releases.
155155
"""
156156
all_releases = []
157-
current_url: str | None = RELEASE_INFORMATION_URL
158-
max_pages = 2
159-
while current_url and max_pages > 0:
160-
max_pages -= 1
161-
response = requests.get(current_url)
162-
response.raise_for_status()
163-
all_releases.extend(response.json())
164-
current_url = None
165-
if "link" in response.headers:
166-
for link in requests.utils.parse_header_links(response.headers["link"]):
167-
if link["rel"] == "next":
168-
current_url = link["url"]
169-
break
157+
for repo in SUPPORTED_REPOS:
158+
current_url: str | None = f"https://api.github.com/repos/{repo}/releases"
159+
max_pages = 2
160+
while current_url and max_pages > 0:
161+
max_pages -= 1
162+
response = requests.get(current_url)
163+
response.raise_for_status()
164+
all_releases.extend(response.json())
165+
current_url = None
166+
if "link" in response.headers:
167+
for link in requests.utils.parse_header_links(response.headers["link"]):
168+
if link["rel"] == "next":
169+
current_url = link["url"]
170+
break
170171

171172
if destination_file:
172173
destination_file.parent.mkdir(parents=True, exist_ok=True)
@@ -200,17 +201,17 @@ def get_release_page_url(release_string: str) -> str:
200201
Return the GitHub Release page URL for a specific release descriptor.
201202
202203
This function can handle:
203-
- A standard release string (e.g., "eip7692@latest").
204+
- A standard release string (e.g., "eip7692@latest") - from execution-spec-tests only.
204205
- A direct asset download link (e.g.,
205206
"https://github.com/ethereum/execution-spec-tests/releases/download/v4.0.0/fixtures_eip7692.tar.gz").
206207
"""
207208
release_information = get_release_information()
208209

209210
# Case 1: If it's a direct GitHub Releases download link,
210211
# find which release in `release_information` has an asset with this exact URL.
211-
if release_string.startswith(
212-
"https://github.com/ethereum/execution-spec-tests/releases/download/"
213-
):
212+
repo_pattern = "|".join(re.escape(repo) for repo in SUPPORTED_REPOS)
213+
regex_pattern = rf"https://github\.com/({repo_pattern})/releases/download/"
214+
if re.match(regex_pattern, release_string):
214215
for release in release_information:
215216
for asset in release.assets.root:
216217
if asset.url == release_string:

whitelist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,3 +857,4 @@ ize
857857
nectos
858858
fibonacci
859859
CPython
860+
legacytests

0 commit comments

Comments
 (0)