diff --git a/.github/workflows/fixtures.yaml b/.github/workflows/fixtures.yaml index a68ed5dcb01..a629a02750a 100644 --- a/.github/workflows/fixtures.yaml +++ b/.github/workflows/fixtures.yaml @@ -44,7 +44,7 @@ jobs: wget -O $GITHUB_WORKSPACE/bin/solc https://binaries.soliditylang.org/${PLATFORM}/$RELEASE_NAME chmod a+x $GITHUB_WORKSPACE/bin/solc echo $GITHUB_WORKSPACE/bin >> $GITHUB_PATH - - name: Run fixtures fill + - name: Run fixtures fill and create fixtures tree hash file shell: bash run: | pip install --upgrade pip @@ -52,6 +52,8 @@ jobs: source env/bin/activate pip install -e . fill ${{ matrix.fill-params }} + dfx --generate-fixtures-tree-only + mv fixtures_tree.json ${{ matrix.name }}_hash_tree.json - name: Create fixtures info file shell: bash run: | @@ -61,20 +63,32 @@ jobs: shell: bash run: | tar -czvf ${{ matrix.name }}.tar.gz ./fixtures - - uses: actions/upload-artifact@v3 + - name: Upload fixtures tar artifact + uses: actions/upload-artifact@v3 with: - name: ${{ matrix.name }} - path: ${{ matrix.name }}.tar.gz + name: ${{ matrix.name }}-artifacts + path: | + ${{ matrix.name }}.tar.gz + - name: Upload separate fixtures tree hash artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.name }}-artifacts + path: | + ${{ matrix.name }}_hash_tree.json release: runs-on: ubuntu-latest needs: build if: startsWith(github.ref, 'refs/tags/') steps: - - name: Download artifacts + - name: Download all artifacts uses: actions/download-artifact@v3 with: path: . - - name: Draft Release + - name: Remove fixtures hash tree files + shell: bash + run: | + rm *_hash_tree.json + - name: Draft release with remaining files uses: softprops/action-gh-release@v1 with: files: './**' diff --git a/setup.cfg b/setup.cfg index 24d82ed00d0..51047a11e28 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ evm_transition_tool = console_scripts = fill = entry_points.fill:main tf = entry_points.tf:main + dfx = entry_points.diff_fixtures:main order_fixtures = entry_points.order_fixtures:main pyspelling_soft_fail = entry_points.pyspelling_soft_fail:main markdownlintcli2_soft_fail = entry_points.markdownlintcli2_soft_fail:main diff --git a/src/entry_points/diff_fixtures.py b/src/entry_points/diff_fixtures.py new file mode 100644 index 00000000000..7ca968589b8 --- /dev/null +++ b/src/entry_points/diff_fixtures.py @@ -0,0 +1,149 @@ +""" +Functions and CLI interface for identifying differences in fixture content based on SHA256 hashes. + +Features include generating SHA256 hash maps for fixture files, excluding the '_info' key for +consistency, and calculating a cumulative hash across all files for quick detection of any changes. + +The CLI interface allows users to detect changes locally during development. + +Example CLI Usage: + ``` + python diff_fixtures.py --input ./fixtures --develop + # or via CLI entry point after package installation + dfx --input ./fixtures + ``` + +CI/CD utilizes the functions to create a json fixture hash map file for the main branch during the +fixture artifact build process, and within the PR branch that a user is developing on. These are +then compared within the PR workflow during each commit to flag any changes in the fixture files. +""" + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Dict, List, Tuple + + +def compute_fixture_hash(fixture_path: Path) -> str: + """ + Generates a sha256 hash of a fixture json files. + The hash for each fixture is calculated without the `_info` key. + """ + with open(fixture_path, "r") as file: + data = json.load(file) + data.pop("_info", None) + return hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest() + + +def compute_cumulative_hash(hashes: List[str]) -> str: + """ + Creates cumulative sha256 hash from a list of hashes. + """ + return hashlib.sha256("".join(sorted(hashes)).encode()).hexdigest() + + +def generate_fixture_tree_json(fixtures_directory: Path, output_file: str, parent_path="") -> None: + """ + Generates a JSON file containing a tree structure of the fixtures directory calculating + cumulative hashes at each folder and file. The tree structure is a nested dictionary used to + compare fixture differences, using the cumulative hash as a quick comparison metric. + """ + + def build_tree(directory: Path, parent_path) -> Tuple[Dict, List[str]]: + """ + Recursively builds a tree structure for fixture directories and files, + calculating cumulative hashes at each sub tree. + """ + directory_contents = {} + all_hashes = [] + + for item in directory.iterdir(): + relative_path = f"{parent_path}/{item.name}" if parent_path else item.name + + if item.is_dir(): + sub_tree, sub_tree_hashes = build_tree(item, relative_path) + directory_contents[item.name] = [ + { + "path": relative_path, + "hash": compute_cumulative_hash(sub_tree_hashes), + "contents": sub_tree, + } + ] + all_hashes.extend(sub_tree_hashes) + elif item.suffix == ".json": + file_hash = compute_fixture_hash(item) + directory_contents[item.name] = [ + { + "path": relative_path, + "hash": file_hash, + } + ] + all_hashes.append(file_hash) + return directory_contents, all_hashes + + tree_contents, tree_hashes = build_tree(fixtures_directory, parent_path) + fixtures_tree = { + "fixtures": { + "cumulative_hash": compute_cumulative_hash(tree_hashes), + "contents": tree_contents, + } + } + with open(output_file, "w") as file: + json.dump(fixtures_tree, file, indent=4) + + +def main(): + """ + CLI interface for comparing fixture differences between the input directory and the main + branch fixture artifacts. + """ + parser = argparse.ArgumentParser( + description=( + "Determines if a diff exists between an input fixtures directory and the most recent " + "built fixtures from the main branch git workflow. " + "Does not provide detailed file changes. " + "If no input directory is provided, `./fixtures` is used as the default. " + "Compares only the non-development fixtures by default." + ) + ) + parser.add_argument( + "--input", + type=str, + default="./fixtures", + help="Input path for the fixtures directory", + ) + parser.add_argument( + "--develop", + action="store_true", + default=False, + help="Compares all fixtures including the development fixtures.", + ) + parser.add_argument( + "--generate-fixtures-tree-only", + action="store_true", + default=False, + help="Generates a fixture tree json file without comparing the input fixtures.", + + ) + parser.add_argument( # To be implemented + "--commit", + type=str, + default=None, + help="The commit hash to compare the input fixtures against.", + ) + args = parser.parse_args() + + input_path = Path(args.input) + if not input_path.exists() or not input_path.is_dir(): + raise FileNotFoundError( + f"Error: The input or default fixtures directory does not exist: {args.input}" + ) + + if args.generate_fixtures_tree_only: + generate_fixture_tree_json(fixtures_directory=input_path, output_file="fixtures_tree.json") + return + + +if __name__ == "__main__": + main() diff --git a/whitelist.txt b/whitelist.txt index bd832f71802..39eb57c7395 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -70,6 +70,7 @@ delitem Dencun dev devnet +dfx difficulty dir dirname @@ -195,6 +196,7 @@ pdb petersburg png Pomerantz +posix ppa ppas pre