fix: qualify patched Python model and cache dependencies
MariaDB persistence qualification / mariadb (push) Has been cancelled
MariaDB persistence qualification / mariadb (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Local qualification only; verify the base image identity in RUNTIME.json before building.
|
||||
FROM jobtracker-p9-sec02-python:runtime
|
||||
USER root
|
||||
COPY wheels /qualification-wheels
|
||||
COPY extras.lock /qualification-extras.lock
|
||||
RUN python -m pip install --no-index --find-links /qualification-wheels --require-hashes --no-cache-dir -r /qualification-extras.lock \
|
||||
&& python -m pip check
|
||||
USER app
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"runtimeImageId": "sha256:4a04ca68fa40b3ec61cb61187b339a4d2895b375b962999e974d75556b5b1331",
|
||||
"qualificationImageId": "sha256:2fe77e47a6c5ecea66764dbd6264049724f24849141e16af5491c2de34af53c1",
|
||||
"applicationLockSha256": "3a3afc529f0076a1b5d67082c9269c22c12f52c47ecf95ce0c76c37b39d8297a",
|
||||
"testLockSha256": "a234316d8612193551a2c91fb19ae0924f5643b9e52edb907ab99dd55b14cdb9",
|
||||
"scope": "Locally built actual source Dockerfile plus six exact hashed test-only extras. Local image IDs, not asserted pullable registry references; no deployment."
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Actual isolated lock/cache failures; never opens a developer or production cache."""
|
||||
from pathlib import Path
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from filelock import FileLock, SoftFileLock
|
||||
from huggingface_hub.utils._fixes import WeakFileLock
|
||||
from verify import verify_fixture
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
manifest = json.loads((HERE / 'ARTIFACTS.json').read_text())['safetensors']
|
||||
source = verify_fixture(Path(sys.argv[1]), manifest)
|
||||
results = []
|
||||
with tempfile.TemporaryDirectory(prefix='jt-model-cache-') as temporary:
|
||||
root = Path(temporary)
|
||||
assert root.stat().st_mode & 0o077 == 0
|
||||
for kind in ['missing', 'corrupt-weights', 'corrupt-config', 'linked', 'extra-code']:
|
||||
directory = root / kind
|
||||
shutil.copytree(source, directory)
|
||||
weights = directory / 'model.safetensors'
|
||||
if kind == 'missing': weights.unlink()
|
||||
elif kind == 'linked':
|
||||
weights.unlink(); weights.symlink_to(source / 'model.safetensors')
|
||||
elif kind == 'extra-code': (directory / 'modeling_unreviewed.py').write_text('raise RuntimeError()')
|
||||
else:
|
||||
file = weights if kind == 'corrupt-weights' else directory / 'config.json'
|
||||
content = bytearray(file.read_bytes()); content[-1] ^= 1; file.write_bytes(content)
|
||||
try: verify_fixture(directory, manifest)
|
||||
except ValueError: results.append({'case': kind, 'rejectedBeforeLoad': True})
|
||||
else: raise AssertionError(kind + ' was accepted')
|
||||
|
||||
child = '''import sys
|
||||
from filelock import FileLock,SoftFileLock,Timeout
|
||||
cls=FileLock if sys.argv[1]=='native' else SoftFileLock
|
||||
try:
|
||||
with cls(sys.argv[2],timeout=0.2): print('acquired')
|
||||
except Timeout: print('timed-out')
|
||||
'''
|
||||
for kind, context in [('native', lambda p: FileLock(p, timeout=1)), ('soft', lambda p: SoftFileLock(p, timeout=1)), ('hub', lambda p: WeakFileLock(p, timeout=1))]:
|
||||
file = root / (kind + '.lock')
|
||||
command = [sys.executable, '-c', child, 'soft' if kind == 'soft' else 'native', str(file)]
|
||||
with context(file):
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=5, check=True)
|
||||
assert result.stdout.strip() == 'timed-out', result.stdout
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=5, check=True)
|
||||
assert result.stdout.strip() == 'acquired', result.stdout
|
||||
results.append({'case': kind + '-cross-process', 'finiteContentionTimeout': True, 'acquiredAfterRelease': True})
|
||||
|
||||
os.environ['AI_SERVICE_SKIP_MODEL_LOAD'] = '0'
|
||||
sys.path.insert(0, '/app')
|
||||
import app
|
||||
from fastapi.testclient import TestClient
|
||||
app.MODEL_NAME = str(root / 'missing-model')
|
||||
assert app._ensure_runtime_loaded() is False
|
||||
assert app.model is app.tokenizer is None and not app.MODEL_LOADED
|
||||
response = TestClient(app.app).post('/summarize', json={'text': 'Synthetic unavailable model input.'})
|
||||
assert response.status_code == 503 and response.json()['detail']
|
||||
results.append({'case': 'missing-offline-model', 'availability503': True, 'noPartialLoadedState': True})
|
||||
verify_fixture(source, manifest)
|
||||
print(json.dumps({'status': 'PASS', 'filelock': importlib.metadata.version('filelock'), 'hub': importlib.metadata.version('huggingface-hub'), 'cases': results, 'freshPrivateCache': True, 'sourceUnchanged': True, 'limits': 'Linux native/soft/Hub locking in one library generation. No shared-cache migration, Windows fallback, full-model cancellation, GPU or live cache claim.'}), flush=True)
|
||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
%PDF-1.7
|
||||
%µ¶
|
||||
|
||||
1 0 obj
|
||||
<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<</Type/Pages/Count 1/Kids[4 0 R]>>
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
<</Font<</helv 5 0 R>>>>
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<</Type/Page/MediaBox[0 0 595 842]/Rotate 0/Resources 3 0 R/Parent 2 0 R/Contents[6 0 R]>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<</Length 195/Filter/FlateDecode>>
|
||||
stream
|
||||
xÚE�ÁN!E÷|`[Êí�&nÜ™°3nÞºpã÷{aL^¥”r8¤ŸôÜ“fáÐ\%GXîßéás|ýæ-÷3¿?ºÂ¡&¾ã b‡¢ šØa²×Xù±E ‡E3cWã"¨Ìel˜ƒ$[„YνVñ…ÝzUØ]A1IäO‡X!hœ×ù Ÿ÷"Xsl&Ube81î</VÊmÝÝü‹á˜~Óê"-×ËònâóÆû§�þš^zzK$ŽGz
|
||||
endstream
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000016 00000 n
|
||||
0000000062 00000 n
|
||||
0000000114 00000 n
|
||||
0000000155 00000 n
|
||||
0000000262 00000 n
|
||||
0000000351 00000 n
|
||||
|
||||
trailer
|
||||
<</Size 7/Root 1 0 R/ID[<C2A562C38EC38E002CC3A5446205C3BB><9B826CB261538573C22FEEA2E273D644>]>>
|
||||
startxref
|
||||
615
|
||||
%%EOF
|
||||
@@ -0,0 +1 @@
|
||||
Ada Lovelace - Åse Østby ærlig erfaring. Backend APIs and reliable data pipelines with Python and C#; platform engineering and testing.
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Resource-bounded actual document/OCR children and real local HTTP startup."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from urllib import request, error
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from verify import verify_fixture
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
manifest = json.loads((HERE / 'ARTIFACTS.json').read_text())['safetensors']
|
||||
verify_fixture('/fixture', manifest)
|
||||
results = []
|
||||
with tempfile.TemporaryDirectory(prefix='jt-parsers-') as temporary:
|
||||
directory = Path(temporary)
|
||||
image = Image.new('RGB', (1000, 180), 'white')
|
||||
ImageDraw.Draw(image).text((40, 50), 'PYTHON CAREER 2026', fill='black', font=ImageFont.load_default(size=48))
|
||||
image.save(directory / 'ocr.png'); image.save(directory / 'ocr.pdf', resolution=150)
|
||||
for file in [*(HERE / 'data').glob('benign.*'), directory / 'ocr.png', directory / 'ocr.pdf']:
|
||||
output = directory / (file.name + '.json')
|
||||
args = [sys.executable, '/app/parser_child.py', '--input', str(file), '--output', str(output), '--extension', file.suffix, '--cpu-seconds', '10', '--memory-bytes', str(512 * 1024 * 1024)]
|
||||
result = subprocess.run(args, capture_output=True, text=True, timeout=20)
|
||||
assert result.returncode == 0, (file.name, result.stdout, result.stderr, output.read_text() if output.exists() else '')
|
||||
parsed = json.loads(output.read_text())
|
||||
assert parsed['ok'] and parsed['text']
|
||||
if file.name.startswith('ocr'):
|
||||
assert all(token in parsed['text'] for token in ['PYTHON','CAREER','2026'])
|
||||
assert parsed['ocr_used'] is True
|
||||
else:
|
||||
assert all(token in parsed['text'] for token in ['Åse','Østby','Python','C#'])
|
||||
results.append({'file':file.name,'sha256':hashlib.sha256(file.read_bytes()).hexdigest(),'text':parsed['text'],'ocrUsed':parsed['ocr_used'],'childExitCode':result.returncode})
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(AI_SERVICE_SKIP_MODEL_LOAD='0', AI_SERVICE_EAGER_MODEL_LOAD='0', AI_SERVICE_TOKEN='p9-local-qualification-only')
|
||||
code = "import sys;sys.path.insert(0,'/app');import app,uvicorn;app.MODEL_NAME='/fixture';uvicorn.run(app.app,host='127.0.0.1',port=18081,log_level='warning')"
|
||||
with Path('/evidence/server.log').open('w') as log:
|
||||
server = subprocess.Popen([sys.executable,'-c',code],env=env,stdout=log,stderr=subprocess.STDOUT)
|
||||
try:
|
||||
deadline=time.monotonic()+30
|
||||
while True:
|
||||
try:
|
||||
with request.urlopen('http://127.0.0.1:18081/health',timeout=1) as response:health=json.load(response)
|
||||
break
|
||||
except (OSError,error.URLError):
|
||||
assert server.poll() is None,'Server exited before readiness'
|
||||
assert time.monotonic()<deadline,'Server readiness deadline exceeded'
|
||||
time.sleep(.1)
|
||||
assert health['model_loaded'] is False
|
||||
body=json.dumps({'text':'Python engineers build reliable accessible career software.','max_length':24,'min_length':8}).encode()
|
||||
try:request.urlopen(request.Request('http://127.0.0.1:18081/summarize',data=body,headers={'Content-Type':'application/json'}),timeout=10)
|
||||
except error.HTTPError as failure:assert failure.code==401
|
||||
else:raise AssertionError('Missing service token accepted')
|
||||
headers={'Content-Type':'application/json','X-Ai-Service-Token':'p9-local-qualification-only'}
|
||||
replies=[]
|
||||
for _ in range(2):
|
||||
with request.urlopen(request.Request('http://127.0.0.1:18081/summarize',data=body,headers=headers),timeout=20) as response:replies.append(json.load(response))
|
||||
assert replies[0]['cached'] is False and replies[1]['cached'] is True
|
||||
assert replies[0]['summary']==replies[1]['summary']
|
||||
boundary='JtSyntheticQualification'
|
||||
upload=(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="synthetic.txt"\r\nContent-Type: text/plain\r\n\r\n'.encode()+(HERE/'data/benign.txt').read_bytes()+f'\r\n--{boundary}--\r\n'.encode())
|
||||
with request.urlopen(request.Request('http://127.0.0.1:18081/extract-text',data=upload,headers={'Content-Type':'multipart/form-data; boundary='+boundary,'X-Ai-Service-Token':'p9-local-qualification-only'}),timeout=20) as response:extracted=json.load(response)
|
||||
assert 'Åse' in extracted['text'] and 'C#' in extracted['text']
|
||||
results.append({'case':'real-http-startup','health200':True,'unauthenticated401':True,'realSummary200':True,'repeatCache':True,'actualUploadExtraction200':True})
|
||||
finally:
|
||||
server.terminate()
|
||||
try:server.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:server.kill();server.wait(timeout=5)
|
||||
assert server.poll() is not None
|
||||
|
||||
data=Path('/usr/share/tesseract-ocr/5/tessdata/eng.traineddata')
|
||||
record={'status':'PASS','cases':results,'tesseractVersion':subprocess.check_output(['tesseract','--version'],text=True).splitlines()[0],'englishDataSha256':hashlib.sha256(data.read_bytes()).hexdigest(),'englishDataBytes':data.stat().st_size,'offline':True,'serverStopped':True,'limits':'Synthetic TXT/DOCX/PDF/image/OCR and small BART CPU HTTP contracts; no production documents, full-size model, GPU, external provider or semantic quality.'}
|
||||
print(json.dumps(record),flush=True)
|
||||
@@ -27,12 +27,20 @@ started = time.monotonic()
|
||||
app.MODEL_NAME = str(fixture)
|
||||
assert app._ensure_runtime_loaded(), app.MODEL_LOAD_ERROR
|
||||
assert app.model.__class__.__name__ == 'BartForConditionalGeneration'
|
||||
assert app.model.training is False
|
||||
assert str(app.device) == 'cpu' and not app.GPU_AVAILABLE
|
||||
text = 'Åse Østby builds accessible Python and C# applications for reliable Norwegian career workflows.'
|
||||
encoded = app.tokenizer(text, return_tensors='pt')
|
||||
assert app.tokenizer.decode(encoded.input_ids[0], skip_special_tokens=True) == text
|
||||
summary = app._model_summarize(text, 24, 8)
|
||||
assert isinstance(summary, str) and summary
|
||||
long_input = app.tokenizer(text * 400, return_tensors='pt', truncation=True, max_length=1024)
|
||||
assert long_input.input_ids.shape == (1, 1024)
|
||||
assert long_input.input_ids.dtype == torch.int64
|
||||
with torch.no_grad():
|
||||
logits = app.model(**long_input, decoder_input_ids=torch.tensor([[2, 0, 10]])).logits
|
||||
assert torch.isfinite(logits).all()
|
||||
assert app._model_summarize(text * 400, 24, 8)
|
||||
client = TestClient(app.app)
|
||||
assert client.get('/health').json()['model_loaded'] is True
|
||||
assert client.post('/summarize', json={'text': text}).status_code == 401
|
||||
@@ -46,4 +54,4 @@ assert first.json()['summary'] == second.json()['summary']
|
||||
verify_fixture(fixture, manifests[kind])
|
||||
elapsed = time.monotonic() - started
|
||||
assert elapsed < 60
|
||||
print(json.dumps({'status': 'PASS', 'kind': kind, 'scope': 'Real synthetic BART/tokenizer/native CPU generation plus sidecar summary/auth/cache contract; no full DistilBART or semantic-quality claim.', 'modelClass': app.model.__class__.__name__, 'device': str(app.device), 'cudaAvailable': torch.cuda.is_available(), 'seconds': elapsed, 'maxRssKiB': resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, 'versions': {name: importlib.metadata.version(name) for name in ['torch','transformers','filelock','huggingface-hub','tokenizers','safetensors']}, 'summary': summary, 'cacheRepeatedRead': True, 'fixtureUnchanged': True}), flush=True)
|
||||
print(json.dumps({'status': 'PASS', 'kind': kind, 'scope': 'Real synthetic BART/tokenizer/native CPU generation plus sidecar summary/auth/cache contract; no full DistilBART or semantic-quality claim.', 'modelClass': app.model.__class__.__name__, 'device': str(app.device), 'cudaAvailable': torch.cuda.is_available(), 'seconds': elapsed, 'maxRssKiB': resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, 'versions': {name: importlib.metadata.version(name) for name in ['torch','transformers','filelock','huggingface-hub','tokenizers','safetensors']}, 'summary': summary, 'bounded1024Tokens': True, 'finiteLogits': True, 'cacheRepeatedRead': True, 'fixtureUnchanged': True}), flush=True)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Run one immutable local qualification lane; retain each attempt and teardown outcome."""
|
||||
from pathlib import Path
|
||||
import datetime
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[5]
|
||||
HERE=Path(__file__).resolve().parent
|
||||
lane=sys.argv[1];attempt=sys.argv[2] if len(sys.argv)>2 else lane
|
||||
assert Path(attempt).name==attempt and attempt not in ['.','..']
|
||||
assert lane in ['safetensors','weights-only','cache','parsers','inventory','sidecar']
|
||||
out=ROOT/'.artifacts/p9-sec-02/qualified'/attempt
|
||||
assert not out.exists(),'Preserve earlier lane evidence.'
|
||||
out.mkdir(parents=True)
|
||||
runtime=json.loads((HERE/'RUNTIME.json').read_text())
|
||||
image=runtime['qualificationImageId']
|
||||
name='jt-sec02-'+lane+'-'+uuid.uuid4().hex[:8]
|
||||
args=['docker','run','--rm','--name',name,'--label','jobtracker.qualification=p9-sec02','--entrypoint','python','--user','65534:65534','--network','none','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges','--pids-limit','128','--cpus','2','--memory','2g','--memory-swap','2g','--tmpfs','/tmp:rw,nosuid,nodev,size=384m,mode=1777']
|
||||
for value in ['HF_HUB_OFFLINE=1','TRANSFORMERS_OFFLINE=1','HF_HUB_DISABLE_TELEMETRY=1','HF_HOME=/tmp/hf-home','OMP_NUM_THREADS=1','MKL_NUM_THREADS=1','OLLAMA_BASE_URL=http://127.0.0.1:11434','OLLAMA_MODEL=','EXTERNAL_AI_ENABLED=0']:
|
||||
args+=['-e',value]
|
||||
args+=['--mount',f'type=bind,src={HERE},dst=/qualification,readonly','--mount',f'type=bind,src={out},dst=/evidence']
|
||||
if lane in ['safetensors','weights-only','cache','parsers']:
|
||||
fixture='artifact-bin' if lane=='weights-only' else 'artifact'
|
||||
args+=['--mount',f'type=bind,src={ROOT/".artifacts/p9-sec-02"/fixture},dst=/fixture,readonly']
|
||||
if lane=='sidecar':args+=['-e','AI_SERVICE_SKIP_MODEL_LOAD=1']
|
||||
args+=[image]
|
||||
if lane in ['safetensors','weights-only']:args+=['/qualification/probe.py',lane,'/fixture']
|
||||
elif lane=='cache':args+=['/qualification/cache_probe.py','/fixture']
|
||||
elif lane=='parsers':args+=['/qualification/parser_probe.py']
|
||||
elif lane=='inventory':args+=['/qualification/runtime_inventory.py']
|
||||
else:args+=['-m','pytest','-q','-o','cache_dir=/tmp/pytest-cache','/app/tests','--junitxml=/evidence/sidecar.xml']
|
||||
started=datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
with (out/'process.log').open('w',encoding='utf-8') as log:
|
||||
try:code=subprocess.run(['rtk','proxy',*args],cwd=ROOT,stdout=log,stderr=subprocess.STDOUT,timeout=180).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code=124
|
||||
check=subprocess.run(['rtk','proxy','docker','inspect','--format','{{index .Config.Labels "jobtracker.qualification"}}',name],capture_output=True,text=True)
|
||||
if check.returncode==0 and check.stdout.strip()=='p9-sec02':subprocess.run(['rtk','proxy','docker','kill',name],capture_output=True)
|
||||
deadline=time.monotonic()+5
|
||||
while True:
|
||||
absent=subprocess.run(['rtk','proxy','docker','inspect',name],capture_output=True).returncode!=0
|
||||
if absent or time.monotonic()>=deadline:break
|
||||
time.sleep(.1)
|
||||
record={'lane':lane,'attempt':attempt,'startedUtc':started,'finishedUtc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'exitCode':code,'command':args,'localImageId':image,'runtimeImageId':runtime['runtimeImageId'],'containerAbsent':absent,'network':'none','readOnly':True,'user':'65534:65534','memoryLimitBytes':2147483648,'cpuLimit':2,'pidsLimit':128,'wallClockDeadlineSeconds':180,'scope':'Actual locally built source image plus exact test-only extras; no app source mount, production cache, external network, registry push or deployment.'}
|
||||
(out/'process.json').write_text(json.dumps(record,indent=2)+'\n')
|
||||
print(json.dumps(record))
|
||||
print((out/'process.log').read_text(encoding='utf-8',errors='replace')[-2500:].encode('ascii','replace').decode())
|
||||
raise SystemExit(code if absent else 1)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Fingerprint actual supported runtime and verify every application lock pin."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
lock=Path('/app/requirements-linux.lock')
|
||||
pins=dict(re.findall(r'^([a-zA-Z0-9_.-]+)==([^\s;]+)',lock.read_text(),re.M))
|
||||
installed={re.sub(r'[-_.]+','-',item.metadata['Name']).lower():item.version for item in importlib.metadata.distributions()}
|
||||
assert len(pins)==55
|
||||
assert all(installed[name]==version for name,version in pins.items())
|
||||
assert not list(Path('/app').glob('.venv*')), 'Development environments leaked into built application image'
|
||||
check=subprocess.run([sys.executable,'-m','pip','check'],capture_output=True,text=True)
|
||||
assert check.returncode==0,check.stdout+check.stderr
|
||||
import torch
|
||||
import transformers
|
||||
data=Path('/usr/share/tesseract-ocr/5/tessdata/eng.traineddata')
|
||||
record={'status':'PASS','python':sys.version,'platform':platform.platform(),'libc':platform.libc_ver(),'applicationLockSha256':hashlib.sha256(lock.read_bytes()).hexdigest(),'applicationPins':pins,'installedDistributions':installed,'extrasOutsideApplicationLock':{name:version for name,version in installed.items() if name not in pins},'pipCheck':check.stdout.strip(),'noDevelopmentEnvironmentsInImage':True,'torchVersion':torch.__version__,'torchBuildCuda':torch.version.cuda,'cudaAvailable':torch.cuda.is_available(),'torchBuild':torch.__config__.show(),'tesseractVersion':subprocess.check_output(['tesseract','--version'],text=True).splitlines()[0],'englishOcrData':{'path':str(data),'bytes':data.stat().st_size,'sha256':hashlib.sha256(data.read_bytes()).hexdigest()},'nativePackages':subprocess.check_output(['dpkg-query','-W'],text=True),'scope':'Actual locally built source Dockerfile and exact55 locked application pins; test-only packages, if any, and bootstrap tools remain explicitly listed separately.'}
|
||||
record['applicationSources']={name:hashlib.sha256((Path('/app')/name).read_bytes()).hexdigest() for name in ['app.py','parser_child.py','requirements.txt','requirements-linux.lock','tests/test_app.py']}
|
||||
print(json.dumps(record),flush=True)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Supplemental Windows lock compatibility in a new local test directory."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
target = str(Path(sys.argv[1]).resolve())
|
||||
sys.path.insert(0, target)
|
||||
import filelock
|
||||
assert filelock.__version__ == '4.2.1'
|
||||
assert Path(filelock.__file__).resolve().is_relative_to(Path(target))
|
||||
child = '''import sys
|
||||
sys.path.insert(0,sys.argv[1])
|
||||
from filelock import FileLock,SoftFileLock,Timeout
|
||||
cls=FileLock if sys.argv[2]=='native' else SoftFileLock
|
||||
try:
|
||||
with cls(sys.argv[3],timeout=0.2): print('acquired')
|
||||
except Timeout: print('timed-out')
|
||||
'''
|
||||
cases = []
|
||||
with tempfile.TemporaryDirectory(prefix='jt-sec02-windows-lock-') as temporary:
|
||||
for kind, cls in [('native', filelock.FileLock), ('soft', filelock.SoftFileLock)]:
|
||||
command=[sys.executable,'-c',child,target,kind,str(Path(temporary)/(kind+'.lock'))]
|
||||
with cls(command[-1],timeout=1):
|
||||
result=subprocess.run(command,capture_output=True,text=True,timeout=5,check=True)
|
||||
assert result.stdout.strip()=='timed-out',result.stdout
|
||||
result=subprocess.run(command,capture_output=True,text=True,timeout=5,check=True)
|
||||
assert result.stdout.strip()=='acquired',result.stdout
|
||||
cases.append({'case':kind,'finiteContentionTimeout':True,'acquiredAfterRelease':True})
|
||||
print(json.dumps({'status':'PASS','platform':sys.platform,'filelock':filelock.__version__,'cases':cases,'limits':'Two fresh same-version Windows lock participants. No Windows shared-cache ACL/symlink security or cross-version migration claim.'}))
|
||||
Reference in New Issue
Block a user