127 lines
8.5 KiB
Python
127 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the future patch in archives, using published 1.0.0 or exact HG source project references."""
|
|
import argparse, datetime, hashlib, io, json, os, shutil, subprocess, tarfile, urllib.request
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
def verify_published_packages(root, consumer, cache, project):
|
|
relative = Path('src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj')
|
|
assert (consumer/relative).read_bytes() == (root/relative).read_bytes(), 'Unexpected source-project override'
|
|
assets = json.loads((consumer/project.parent/'obj/project.assets.json').read_text())
|
|
assert all(Path(p).resolve() == cache.resolve() for p in assets['packageFolders'])
|
|
proof = {'no_source_project_reference_override': True, 'packages': {}}
|
|
for name in ['Client', 'Contracts']:
|
|
ident = 'ZB.MOM.WW.HistorianGateway.' + name
|
|
library = assets['libraries'][ident + '/1.0.0']
|
|
assert library['type'] == 'package'
|
|
package = cache/ident.lower()/'1.0.0'
|
|
provenance = json.loads((package/'.nupkg.metadata').read_text())
|
|
assert provenance['source'].startswith('https://gitea.dohertylan.com/api/packages/dohertj2/nuget')
|
|
dll = package/'lib/net10.0'/(ident + '.dll')
|
|
copied = consumer/project.parent/'bin/Release/net10.0'/(ident + '.dll')
|
|
digest = hashlib.sha256(dll.read_bytes()).hexdigest()
|
|
assert digest == hashlib.sha256(copied.read_bytes()).hexdigest()
|
|
nuspec = ET.parse(package/(ident.lower() + '.nuspec'))
|
|
assert nuspec.findtext('.//{*}version') == '1.0.0'
|
|
repository = nuspec.find('.//{*}repository')
|
|
proof['packages'][ident] = {'version': '1.0.0', 'asset_type': 'package',
|
|
'source': provenance['source'], 'dll_sha256': digest,
|
|
'nupkg_sha256': hashlib.sha256((package/(ident.lower() + '.1.0.0.nupkg')).read_bytes()).hexdigest(),
|
|
'repository': repository.attrib if repository is not None else None,
|
|
'test_bin_dll_matches_package': True}
|
|
return proof
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--consumer-root', type=Path, required=True)
|
|
parser.add_argument('--hg-root', type=Path, required=True)
|
|
parser.add_argument('--hg-commit', default='e4321915ebc5e0704a68ecad21b85c1cc1c80d6b')
|
|
parser.add_argument('--output', type=Path, required=True)
|
|
a = parser.parse_args(); root = a.consumer_root.resolve(); out = a.output.resolve()
|
|
if out.is_relative_to(root) or out.is_relative_to(a.hg_root.resolve()):
|
|
raise ValueError('Use an output directory outside both checkouts so archived HG does not inherit consumer central-package settings.')
|
|
out.mkdir(parents=True, exist_ok=False)
|
|
patch = Path(__file__).with_name('consumer-1.0.0.patch').resolve()
|
|
project = Path('tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj')
|
|
sha = subprocess.check_output(['git','-C',str(root),'rev-parse','HEAD'],text=True).strip()
|
|
hg_sha = subprocess.check_output(['git','-C',str(a.hg_root),'rev-parse',a.hg_commit],text=True).strip()
|
|
def archive(repo, rev, target):
|
|
target.mkdir()
|
|
data = subprocess.check_output(['git','-C',str(repo),'archive',rev])
|
|
with tarfile.open(fileobj=io.BytesIO(data)) as tar: tar.extractall(target, filter='data')
|
|
|
|
versions = {}
|
|
for name in ['client','contracts']:
|
|
url = 'https://gitea.dohertylan.com/api/packages/dohertj2/nuget/package/zb.mom.ww.historiangateway.' + name + '/index.json'
|
|
versions[name] = json.load(urllib.request.urlopen(url,timeout=20))['versions']
|
|
published = all('1.0.0' in v for v in versions.values())
|
|
metadata = {'utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'consumer_commit':sha,'hg_snapshot':hg_sha,
|
|
'mode':'published-1.0.0' if published else 'unreleased-source-project-reference',
|
|
'feed_versions':versions,'patch_sha256':hashlib.sha256(patch.read_bytes()).hexdigest(),
|
|
'published_package_verification':published,'normal_package_cache_modified':False}
|
|
(out/'metadata.json').write_text(json.dumps(metadata,indent=2))
|
|
consumer = out/'consumer'; archive(root,sha,consumer)
|
|
subprocess.run(['git','init','-q',str(consumer)],check=True)
|
|
subprocess.run(['git','-C',str(consumer),'apply',str(patch)],check=True)
|
|
if not published:
|
|
hg = out/'historiangw-source'; archive(a.hg_root,hg_sha,hg)
|
|
adapter = consumer/'src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj'
|
|
tree = ET.parse(adapter)
|
|
removed = 0
|
|
for group in tree.getroot().findall('ItemGroup'):
|
|
for item in list(group):
|
|
if item.tag=='PackageReference' and item.get('Include') in ['ZB.MOM.WW.HistorianGateway.Client','ZB.MOM.WW.HistorianGateway.Contracts']:
|
|
group.remove(item);removed += 1
|
|
assert removed==2
|
|
group = ET.SubElement(tree.getroot(),'ItemGroup')
|
|
for relative in ['clients/dotnet/ZB.MOM.WW.HistorianGateway.Client/ZB.MOM.WW.HistorianGateway.Client.csproj',
|
|
'src/ZB.MOM.WW.HistorianGateway.Contracts/ZB.MOM.WW.HistorianGateway.Contracts.csproj']:
|
|
ET.SubElement(group,'ProjectReference',Include=str(hg/relative))
|
|
tree.write(adapter,encoding='unicode')
|
|
metadata['temporary_override']='Only archived Gateway csproj: two package references replaced by exact archived HG project references; no package packed or published.'
|
|
# Copy cached dependencies independently, preferably APFS copy-on-write, into the isolated package root.
|
|
cache = out/'packages'; cache.mkdir()
|
|
assets = json.loads((root/project.parent/'obj/project.assets.json').read_text())
|
|
normal = Path(next(iter(assets['packageFolders'])))
|
|
for library in assets['libraries'].values():
|
|
if library.get('type')!='package':continue
|
|
relative = library['path']; source=normal/relative; target=cache/relative
|
|
target.parent.mkdir(parents=True,exist_ok=True)
|
|
if target.exists():continue
|
|
r=subprocess.run(['cp','-cR',str(source),str(target)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
|
if r.returncode: shutil.copytree(source,target)
|
|
config = ET.parse(root/'NuGet.config')
|
|
for source in config.getroot().find('packageSources'):
|
|
if source.get('key')=='local-mxgw':
|
|
local = root/'nuget-packages'
|
|
if not local.exists():local=out/'empty-local';local.mkdir()
|
|
source.set('value',str(local))
|
|
config.write(out/'NuGet.config',encoding='unicode')
|
|
env=dict(os.environ,NUGET_PACKAGES=str(cache),NUGET_HTTP_CACHE_PATH=str(out/'http-cache'),DOTNET_CLI_HOME=str(out/'dotnet-home'),
|
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE='1',DOTNET_GENERATE_ASPNET_CERTIFICATE='false',DOTNET_CLI_TELEMETRY_OPTOUT='1')
|
|
commands=[]
|
|
def run(args,name):
|
|
with (out/name).open('w') as log:r=subprocess.run(args,cwd=consumer,env=env,stdout=log,stderr=subprocess.STDOUT)
|
|
commands.append({'argv':args,'exit':r.returncode,'log':name})
|
|
(out/'commands.json').write_text(json.dumps(commands,indent=2));print(name,'exit',r.returncode,flush=True)
|
|
return r.returncode
|
|
restore = run(['dotnet','restore',str(project),'--configfile',str(out/'NuGet.config'),'--packages',str(cache)],'restore.log')
|
|
metadata['restore_exit']=restore
|
|
if restore:
|
|
(out/'metadata.json').write_text(json.dumps(metadata,indent=2));raise SystemExit(restore)
|
|
results=out/'test-results'
|
|
test = run(['dotnet','test',str(project),'-c','Release','--no-restore','--logger','trx','--results-directory',str(results)],'test.log')
|
|
guard_env=env.copy();guard_env['MIN_EXECUTED']='1'
|
|
with (out/'guard.log').open('w') as log:
|
|
guard=subprocess.run(['bash',str(consumer/'scripts/ci/assert-not-all-skipped.sh'),*[str(p) for p in results.glob('*.trx')]],env=guard_env,stdout=log,stderr=subprocess.STDOUT)
|
|
metadata.update({'test_exit':test,'guard_exit':guard.returncode})
|
|
ns={'t':'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'}
|
|
metadata['results']=[]
|
|
for trx in results.glob('*.trx'):
|
|
xml=ET.parse(trx);metadata['results'].append(xml.find('.//t:Counters',ns).attrib)
|
|
metadata['tls_cases']=[{'name':r.get('testName'),'outcome':r.get('outcome'),'output':'\n'.join(p.text or '' for p in r.findall('.//t:StdOut',ns)),
|
|
'error':r.findtext('.//t:Message','',ns)} for r in xml.findall('.//t:UnitTestResult',ns) if 'HistorianGatewayTlsPolicyWireTests' in r.get('testName','')]
|
|
if published:
|
|
metadata['package_proof'] = verify_published_packages(root, consumer, cache, project)
|
|
(out/'metadata.json').write_text(json.dumps(metadata,indent=2));print(json.dumps(metadata,indent=2),flush=True)
|
|
raise SystemExit(test if test else guard.returncode)
|