Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,8 @@ jobs:
echo "http $status, found $count of $expected expected files for ${PCAPKIT_VERSION} on PyPI"
echo "PCAPKIT_PYPI_COMPLETE=$complete" >> "$GITHUB_OUTPUT"

# Anaconda evidence, same shape as PyPI's above but for the ``conda``
# matrix (linux-64 and osx-64, each Python read above). Unlike PyPI's, this
# Anaconda evidence, same shape as PyPI's above but for the ``conda`` matrix
# (each platform its legs publish, each Python read above). Unlike PyPI's, this
# endpoint reported ``cf-cache-status: DYNAMIC`` on every request checked
# while writing this, i.e. not cached -- so there is no staleness direction to
# reason about here. This is the job-level cost saver mirroring ``pypi``'s;
Expand All @@ -488,11 +488,11 @@ jobs:
run: |
set -euo pipefail

# linux-64 and osx-64, for each supported version. osx-arm64 is
# published too and not counted, as it was not when ``macos-latest``
# uploaded it beside the osx-64 package it converted (#1822).
# Each platform the ``conda`` legs publish, for each supported version.
export PCAPKIT_CONDA_SUBDIRS="linux-64 osx-64 osx-arm64"
read -r -a subdirs <<< "$PCAPKIT_CONDA_SUBDIRS"
read -r -a builds <<< "$PCAPKIT_CONDA_BUILDS"
expected=$(( 2 * ${#builds[@]} ))
expected=$(( ${#subdirs[@]} * ${#builds[@]} ))
# Same coupling risk, and the same fix, as the PyPI check above.
status="$(curl -s --retry 3 --retry-connrefused \
-o /tmp/anaconda_release.json -w '%{http_code}' \
Expand All @@ -507,7 +507,7 @@ jobs:
"import json, os" \
"with open('/tmp/anaconda_release.json') as f:" \
" data = json.load(f)" \
"platforms = {'linux-64', 'osx-64'}" \
"platforms = set(os.environ['PCAPKIT_CONDA_SUBDIRS'].split())" \
"py_tags = set(os.environ['PCAPKIT_CONDA_BUILDS'].split())" \
"seen = {" \
" (d.get('attrs', {}).get('subdir'), tag)" \
Expand Down
6 changes: 3 additions & 3 deletions docs/source/contributing/releasing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,9 @@ checks *is* the one it is about to create:
already idempotent (``skip-existing: true`` on every upload step), so the
check exists only to save the cost of the build matrix, not for correctness.
* ``conda`` compares Anaconda's aggregate count against the expected one (the
``linux-64`` and ``osx-64`` files for each Python version of its matrix; the
``osx-arm64`` leg's are published but not counted,
`#1822 <https://github.com/JarryShaw/PyPCAPKit/issues/1822>`__) as the same
``linux-64``, ``osx-64`` and ``osx-arm64`` files for each Python version of
its matrix, one platform per ``os`` leg,
`#1922 <https://github.com/JarryShaw/PyPCAPKit/issues/1922>`__) as the same
kind of job-level cost saver, and *also*
checks, per matrix leg, whether that leg's own platform/Python distribution
is already there -- this second check is the actual correctness fix, because
Expand Down
126 changes: 126 additions & 0 deletions tests/project/test_conda_completeness_counts_every_leg_1922.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""``version_check``'s Anaconda completeness check counts every platform the ``conda`` legs publish (#1922).

* The platforms it counts are the ones the ``conda`` job's ``os`` legs map to,
read from that job's matrix and its ``case "${{ matrix.os }}"`` arms.
* Run as the workflow runs it, with ``curl`` stubbed, a release reads complete
only with each of those platforms for each build, so a missing ``osx-arm64``
upload reads incomplete and the ``conda`` gate runs the leg that lacks it.

"""

from __future__ import annotations

import json
import os
import pathlib
import re
import shutil
import subprocess # nosec: B404
import tempfile
import unittest
from typing import TYPE_CHECKING

try:
import yaml
except ImportError: # pragma: no cover
yaml = None # type: ignore[assignment]

if TYPE_CHECKING:
from typing import Any

ROOT = pathlib.Path(__file__).resolve().parents[2]
WORKFLOW = ROOT / '.github' / 'workflows' / 'create-release.yml'
STEP = 'Check Anaconda publish completeness'
BUILDS = ('py313', 'py314')

#: A ``case "${{ matrix.os }}" in`` arm setting ``target_platform``.
ARM = re.compile(r'^\s*([\w.-]+)\)\s*\n\s*target_platform="([^"]*)"', re.MULTILINE)
#: The check's own list of platforms, which sets both its count and its set.
SUBDIRS = re.compile(r'^\s*export PCAPKIT_CONDA_SUBDIRS="([^"]*)"\s*$', re.MULTILINE)


def jobs() -> 'dict[str, Any]':
if yaml is None: # pragma: no cover
raise unittest.SkipTest('PyYAML, from the test extra, is not installed')
return yaml.safe_load(WORKFLOW.read_text(encoding='utf-8'))['jobs']


def check() -> 'dict[str, Any]':
return next(step for step in jobs()['version_check']['steps'] if step.get('name') == STEP)


def published() -> 'set[str]':
"""The platform each ``conda`` leg publishes, from its matrix and each ``case`` of its steps."""
spec = jobs()['conda']
labels = spec['strategy']['matrix']['os']
found = set()
for step in spec['steps']:
for block in re.findall(r'case "\$\{\{ matrix\.os \}\}" in\n(.*?)\n\s*esac', str(step.get('run', '')),
re.DOTALL):
arms = dict(ARM.findall(block))
found.add(frozenset(arms[label] for label in labels))
assert len(found) == 1, found
return set(found.pop())


class TheCheckCountsTheLegs(unittest.TestCase):
"""What the check counts is what the legs publish."""

def test_the_check_lists_each_published_platform(self) -> None:
listed = SUBDIRS.findall(check()['run'])
self.assertEqual(len(listed), 1, 'the check names its platforms in one PCAPKIT_CONDA_SUBDIRS list')
self.assertEqual(sorted(listed[0].split()), sorted(published()))
self.assertIn('osx-arm64', published())


@unittest.skipIf(shutil.which('bash') is None or shutil.which('python3') is None,
'bash and python3 are needed to run the workflow shell')
class TheCheckRunsAsTheWorkflowRunsIt(unittest.TestCase):
"""The step's script, with ``curl`` answering from a fake Anaconda release."""

def complete(self, distributions: 'list[tuple[str, str]]') -> 'tuple[str, str]':
"""``PCAPKIT_CONDA_COMPLETE`` and the log for a release holding these (platform, build) pairs."""
step = check()
self.assertEqual(sorted(step['env']), ['PCAPKIT_CONDA_BUILDS', 'PCAPKIT_VERSION'])
with tempfile.TemporaryDirectory(prefix='pcapkit-1922-') as tmp:
scratch = pathlib.Path(tmp)
reply = scratch / 'reply.json'
reply.write_text(json.dumps({'distributions': [{'attrs': {'subdir': platform, 'build': f'{build}_0'}}
for platform, build in distributions]}), encoding='utf-8')
curl = scratch / 'curl'
curl.write_text('#!/bin/bash\nwhile [ $# -gt 0 ]; do [ "$1" = -o ] && cp "$REPLY" "$2"; shift; done\n'
'printf 200\n', encoding='utf-8')
curl.chmod(0o755)
output = scratch / 'output'
output.write_text('', encoding='utf-8')
result = subprocess.run( # nosec: B603 B607
['bash', '-e', '-c', step['run'].replace('/tmp/anaconda_release.json', str(scratch / 'release.json'))],
env=dict(os.environ, PATH=f'{scratch}{os.pathsep}{os.environ["PATH"]}', REPLY=str(reply),
GITHUB_OUTPUT=str(output), PCAPKIT_VERSION='1.5.0', PCAPKIT_CONDA_BUILDS=' '.join(BUILDS)),
capture_output=True, text=True, check=False, timeout=60)
self.assertEqual(result.returncode, 0, result.stderr)
written = dict(line.split('=', 1) for line in output.read_text(encoding='utf-8').split())
return written['PCAPKIT_CONDA_COMPLETE'], result.stdout

def test_every_platform_for_every_build_reads_complete(self) -> None:
every = [(platform, build) for platform in sorted(published()) for build in BUILDS]
state, log = self.complete(every)
self.assertEqual(state, 'true', log)
self.assertIn(f'found {len(every)} of {len(every)} expected', log)

def test_a_missing_platform_reads_incomplete(self) -> None:
for missing in sorted(published()):
rest = [(platform, build) for platform in sorted(published() - {missing}) for build in BUILDS]
with self.subTest(missing=missing):
self.assertEqual(self.complete(rest)[0], 'false')

def test_a_missing_osx_arm64_build_reads_incomplete(self) -> None:
every = [(platform, build) for platform in sorted(published()) for build in BUILDS]
state, log = self.complete([pair for pair in every if pair != ('osx-arm64', BUILDS[-1])])
self.assertEqual(state, 'false', log)
self.assertIn(f'found {len(every) - 1} of {len(every)} expected', log)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion tests/project/test_python_versions_1660.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ def complete(self, distributions: 'list[tuple[str, str]]') -> 'str':

def test_each_platform_needs_each_supported_build(self) -> None:
builds = live_outputs()['conda-builds'].split()
every = [(platform, build) for platform in ('linux-64', 'osx-64') for build in builds]
every = [(platform, build) for platform in ('linux-64', 'osx-64', 'osx-arm64') for build in builds]
ktlo = [('linux-64', 'py' + version.replace('.', ''))
for version in script().tier(script().load(), 'cpython', 'ktlo')]
self.assertEqual(self.complete(every), 'true')
Expand Down
3 changes: 2 additions & 1 deletion tests/project/test_release_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2378,10 +2378,11 @@ def test_pypi_evidence_reads_the_per_version_endpoint(self) -> None:
self.assertIn('pypi.org/pypi/pypcapkit/', block)
self.assertIn("data.get('urls'", block)

def test_conda_evidence_checks_both_platforms(self) -> None:
def test_conda_evidence_checks_every_platform(self) -> None:
block = self.jobs['version_check']
self.assertIn('linux-64', block)
self.assertIn('osx-64', block)
self.assertIn('osx-arm64', block)


class TestGateExpressionsEvaluateCorrectly(WorkflowMixin, unittest.TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/project/test_release_tags_the_run_sha_1634.py
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,7 @@ def test_a_finished_release_reads_complete_after_the_tiers_grow(self) -> None:
files = [f'pypcapkit-{self.VERSION}-{tag}-none-any.whl' for tag in released['pypi-wheels'].split()]
pypi = {'urls': [{'filename': name} for name in files + [f'pypcapkit-{self.VERSION}.tar.gz']]}
conda = {'distributions': [{'attrs': {'subdir': platform, 'build': f'{build}_0'}}
for platform in ('linux-64', 'osx-64')
for platform in ('linux-64', 'osx-64', 'osx-arm64')
for build in released['conda-builds'].split()]}
step = 'Check PyPI publish completeness'
self.assertEqual(self.complete(step, written['pypi-wheels'], '', pypi), 'true')
Expand Down
Loading