pytest-allure-host 0.1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_allure_host/__init__.py +8 -0
- pytest_allure_host/cli.py +163 -22
- pytest_allure_host/publisher.py +1311 -359
- pytest_allure_host/templates.py +158 -0
- pytest_allure_host/utils.py +24 -0
- {pytest_allure_host-0.1.2.dist-info → pytest_allure_host-2.0.0.dist-info}/METADATA +88 -1
- pytest_allure_host-2.0.0.dist-info/RECORD +13 -0
- pytest_allure_host-0.1.2.dist-info/RECORD +0 -12
- {pytest_allure_host-0.1.2.dist-info → pytest_allure_host-2.0.0.dist-info}/WHEEL +0 -0
- {pytest_allure_host-0.1.2.dist-info → pytest_allure_host-2.0.0.dist-info}/entry_points.txt +0 -0
- {pytest_allure_host-0.1.2.dist-info → pytest_allure_host-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
"""Template and asset constants for Allure hosting publisher.
|
2
|
+
|
3
|
+
Separating large inline CSS/JS blobs from logic code improves readability and
|
4
|
+
keeps `publisher.py` focused on assembling manifests and uploading artifacts.
|
5
|
+
|
6
|
+
Constants here are intentionally raw (no minification beyond what was already
|
7
|
+
present) to avoid altering runtime behaviour. Any future templating engine
|
8
|
+
integration can replace these with loader functions while tests assert for
|
9
|
+
key sentinel substrings.
|
10
|
+
"""
|
11
|
+
|
12
|
+
# flake8: noqa # Long lines expected for embedded assets
|
13
|
+
|
14
|
+
# ---------------------------- Runs Index CSS ----------------------------
|
15
|
+
RUNS_INDEX_CSS_BASE = (
|
16
|
+
":root{--bg:#fff;--bg-alt:#f6f8fa;--border:#d0d7de;--accent:#0366d6;--pass:#2e7d32;--fail:#d32f2f;--broken:#ff9800;--warn:#d18f00;--code-bg:#f2f4f7;--text:#111;--text-dim:#555;}"
|
17
|
+
"@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--bg-alt:#161b22;--border:#30363d;--accent:#58a6ff;--text:#e6edf3;--text-dim:#9aa3b1;--code-bg:#1c232b;}}"
|
18
|
+
)
|
19
|
+
|
20
|
+
RUNS_INDEX_CSS_TABLE = (
|
21
|
+
"body{font-family:system-ui;margin:1.5rem;background:var(--bg);color:var(--text);}" # noqa: E501
|
22
|
+
"table{border-collapse:collapse;width:100%;font-size:13px;}"
|
23
|
+
"th,td{padding:.45rem .55rem;border:1px solid var(--border);text-align:left;}"
|
24
|
+
"thead th{background:var(--bg-alt);position:sticky;top:0;z-index:2;}"
|
25
|
+
"tbody tr:nth-child(even){background:var(--bg-alt);}" # noqa: E501
|
26
|
+
"code{background:var(--code-bg);padding:2px 4px;border-radius:3px;font-size:12px;}"
|
27
|
+
)
|
28
|
+
|
29
|
+
RUNS_INDEX_CSS_MISC = (
|
30
|
+
".stats{font-size:12px;color:var(--text-dim);margin:.25rem 0 0;}"
|
31
|
+
".controls{display:flex;flex-wrap:wrap;gap:.5rem;margin:.6rem 0 1rem;align-items:flex-start;}"
|
32
|
+
".controls-section{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;}"
|
33
|
+
".controls input[type=text]{padding:.4rem .55rem;font-size:13px;border:1px solid var(--border);background:var(--bg-alt);color:var(--text);}" # noqa: E501
|
34
|
+
".badge{display:inline-block;padding:2px 6px;border-radius:999px;font-size:11px;font-weight:600;}"
|
35
|
+
".badge-pass{background:var(--pass);color:#fff;}"
|
36
|
+
".badge-fail{background:var(--fail);color:#fff;}"
|
37
|
+
".badge-broken{background:var(--broken);color:#fff;}"
|
38
|
+
"tbody tr.row-fail{outline:2px solid var(--fail);outline-offset:-2px;}"
|
39
|
+
".link-btn.copied{color:var(--pass);}" # existing style piece
|
40
|
+
".col-hidden{display:none !important;}" # hide columns when toggled
|
41
|
+
"#col-panel button{font-size:11px;}" # existing style piece
|
42
|
+
".pfb-pass{color:var(--pass);font-weight:600;}"
|
43
|
+
".pfb-fail{color:var(--fail);font-weight:600;}"
|
44
|
+
".pfb-broken{color:var(--broken);font-weight:600;}"
|
45
|
+
".dense td, .dense th{padding:.25rem .35rem !important;font-size:12px;}"
|
46
|
+
".tag-chip{display:inline-block;background:var(--bg-alt);border:1px solid var(--border);padding:2px 5px;margin:0 4px 3px 0;border-radius:12px;font-size:11px;cursor:pointer;user-select:none;}"
|
47
|
+
".tag-chip:hover{background:var(--accent);color:#fff;border-color:var(--accent);}"
|
48
|
+
"#spark-wrap{display:flex;flex-direction:column;gap:.25rem;}"
|
49
|
+
"#spark{width:140px;height:26px;display:block;}"
|
50
|
+
"\n.sr-only{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;}"
|
51
|
+
)
|
52
|
+
|
53
|
+
# Additional enhancements CSS (density toggle, in-progress marker, notice)
|
54
|
+
RUNS_INDEX_CSS_ENH = (
|
55
|
+
"#runs-table.dense td{padding:4px 6px}" # compact row density
|
56
|
+
"#runs-table.dense th{padding:4px 6px}" # ensure headers match
|
57
|
+
".run--inprogress .time::after{content:' • running';font-style:italic;color:#666;}"
|
58
|
+
"#notice{margin:8px 0;color:#444;font-style:italic;}"
|
59
|
+
)
|
60
|
+
|
61
|
+
# ---------------------------- Runs Index JS ----------------------------
|
62
|
+
# NOTE: INIT and BATCH values are injected separately by publisher.
|
63
|
+
RUNS_INDEX_JS = (
|
64
|
+
"(function(){"
|
65
|
+
"const tbl=document.getElementById('runs-table');"
|
66
|
+
"const filter=document.getElementById('run-filter');"
|
67
|
+
"const stats=document.getElementById('stats');"
|
68
|
+
"const pfbStats=document.getElementById('pfb-stats');"
|
69
|
+
"const onlyFail=document.getElementById('only-failing');"
|
70
|
+
"const clearBtn=document.getElementById('clear-filter');"
|
71
|
+
"const themeBtn=document.getElementById('theme-toggle');"
|
72
|
+
"const accentBtn=document.getElementById('accent-toggle');"
|
73
|
+
"const colBtn=document.getElementById('col-toggle');"
|
74
|
+
"const densityBtn=document.getElementById('density-toggle');"
|
75
|
+
"const tzBtn=document.getElementById('tz-toggle');"
|
76
|
+
"let localTime=false;"
|
77
|
+
"let colPanel=null;"
|
78
|
+
"const LS='ah_runs_';"
|
79
|
+
"function lsGet(k){try{return localStorage.getItem(LS+k);}catch(e){return null;}}"
|
80
|
+
"function lsSet(k,v){try{localStorage.setItem(LS+k,v);}catch(e){}}"
|
81
|
+
"function hidden(){return [...tbl.tBodies[0].querySelectorAll('tr.pr-hidden')];}"
|
82
|
+
"function updateLoadButton(){const hiddenRows=hidden();const loadBtn=document.getElementById('load-more');if(!loadBtn)return;if(hiddenRows.length){loadBtn.style.display='inline-block';loadBtn.textContent='Load more ('+hiddenRows.length+')';}else{loadBtn.style.display='none';}}"
|
83
|
+
"function revealNextBatch(batch){hidden().slice(0,batch).forEach(r=>r.classList.remove('pr-hidden'));updateLoadButton();}" # progressive reveal
|
84
|
+
"function failingTotal(){return [...tbl.tBodies[0].rows].reduce((a,r)=>a+ (Number(r.dataset.failed||0)>0?1:0),0);}"
|
85
|
+
"function applyStats(){const total=tbl.tBodies[0].rows.length;const rows=[...tbl.tBodies[0].rows];const vis=rows.filter(r=>r.style.display!=='none');stats.textContent=vis.length+' / '+total+' shown';let p=0,f=0,b=0;vis.forEach(r=>{p+=Number(r.dataset.passed||0);f+=Number(r.dataset.failed||0);b+=Number(r.dataset.broken||0);});pfbStats.textContent=' P:'+p+' F:'+f+' B:'+b;}"
|
86
|
+
"function applyFooter(){const total=tbl.tBodies[0].rows.length;const hid=hidden().length;const el=document.getElementById('footer-stats');if(el){el.textContent=(total-hid)+' / '+total+' loaded';}}"
|
87
|
+
"function applyFilter(){const loadBtn=document.getElementById('load-more');const raw=filter.value.trim().toLowerCase();const tokens=raw.split(/\\s+/).filter(Boolean);const onlyF=onlyFail.checked;if(tokens.length&&document.querySelector('.pr-hidden')){hidden().forEach(r=>r.classList.remove('pr-hidden'));updateLoadButton();}const rows=[...tbl.tBodies[0].rows];rows.forEach(r=>{const hay=r.getAttribute('data-search')||'';const hasTxt=!tokens.length||tokens.every(t=>hay.indexOf(t)>-1);const failing=Number(r.dataset.failed||0)>0;r.style.display=(hasTxt&&(!onlyF||failing))?'':'none';if(failing){r.classList.add('failing-row');}else{r.classList.remove('failing-row');}});document.querySelectorAll('tr.row-active').forEach(x=>x.classList.remove('row-active'));if(tokens.length===1){const rid=tokens[0];const match=[...tbl.tBodies[0].rows].find(r=>r.querySelector('td.col-run_id code')&&r.querySelector('td.col-run_id code').textContent.trim().toLowerCase()===rid);if(match)match.classList.add('row-active');}applyStats();}"
|
88
|
+
"function relFmt(sec){if(sec<60)return Math.floor(sec)+'s';sec/=60;if(sec<60)return Math.floor(sec)+'m';sec/=60;if(sec<24)return Math.floor(sec)+'h';sec/=24;if(sec<7)return Math.floor(sec)+'d';const w=Math.floor(sec/7);if(w<4)return w+'w';const mo=Math.floor(sec/30);if(mo<12)return mo+'mo';return Math.floor(sec/365)+'y';}"
|
89
|
+
"function updateAges(){const now=Date.now()/1000;tbl.tBodies[0].querySelectorAll('td.age').forEach(td=>{const ep=Number(td.getAttribute('data-epoch'));if(!ep){td.textContent='-';return;}td.textContent=relFmt(now-ep);});}"
|
90
|
+
"const loadBtn=document.getElementById('load-more');if(loadBtn){loadBtn.addEventListener('click',()=>{revealNextBatch(Number(loadBtn.getAttribute('data-batch'))||300);applyFilter();lsSet('loaded',String(tbl.tBodies[0].rows.length-hidden().length));});}"
|
91
|
+
"// Infinite scroll (observer)"
|
92
|
+
"if('IntersectionObserver' in window && loadBtn){const io=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting && hidden().length){revealNextBatch(Number(loadBtn.getAttribute('data-batch'))||300);applyFilter();}});},{root:null,rootMargin:'120px'});io.observe(loadBtn);}"
|
93
|
+
"filter.addEventListener('input',()=>{applyFilter();lsSet('filter',filter.value);});"
|
94
|
+
"filter.addEventListener('keydown',e=>{if(e.key==='Enter'){applyFilter();}});"
|
95
|
+
"onlyFail.addEventListener('change',()=>{applyFilter();lsSet('onlyFail',onlyFail.checked?'1':'0');});"
|
96
|
+
"clearBtn&&clearBtn.addEventListener('click',()=>{filter.value='';onlyFail.checked=false;applyFilter();filter.focus();});"
|
97
|
+
"const ACCENTS=['#0366d6','#6f42c1','#d32f2f','#1b7f3b','#b08800'];"
|
98
|
+
"function applyAccent(c){document.documentElement.style.setProperty('--accent',c);lsSet('accent',c);}" # noqa: E501
|
99
|
+
"accentBtn&&accentBtn.addEventListener('click',()=>{const cur=lsGet('accent')||ACCENTS[0];const idx=(ACCENTS.indexOf(cur)+1)%ACCENTS.length;applyAccent(ACCENTS[idx]);});"
|
100
|
+
"function applyTheme(mode){document.documentElement.classList.remove('force-light','force-dark');if(mode==='light'){document.documentElement.classList.add('force-light');}else if(mode==='dark'){document.documentElement.classList.add('force-dark');}lsSet('theme',mode);}" # noqa: E501
|
101
|
+
"themeBtn&&themeBtn.addEventListener('click',()=>{const cur=lsGet('theme')||'auto';const next=cur==='auto'?'dark':(cur==='dark'?'light':'auto');applyTheme(next);themeBtn.textContent='Theme('+next+')';});"
|
102
|
+
"densityBtn&&densityBtn.addEventListener('click',()=>{const dense=tbl.classList.toggle('dense');lsSet('dense',dense?'1':'0');densityBtn.textContent=dense?'Dense(-)':'Dense(+)' ;});"
|
103
|
+
"tzBtn&&tzBtn.addEventListener('click',()=>{localTime=!localTime;updateTimeCells();tzBtn.textContent=localTime?'UTC':'Local';lsSet('tz',localTime?'local':'utc');});"
|
104
|
+
"function updateTimeCells(){[...tbl.tBodies[0].rows].forEach(r=>{const utcCell=r.querySelector('td.col-utc');if(!utcCell)return;const ep=Number(r.dataset.epoch||0);if(!ep){utcCell.textContent='-';return;}if(localTime){const d=new Date(ep*1000);utcCell.textContent=d.toLocaleString();}else{const d=new Date(ep*1000);utcCell.textContent=d.toISOString().replace('T',' ').slice(0,19);} });}"
|
105
|
+
"function extract(r,col){if(col.startsWith('meta:')){const idx=[...tbl.tHead.querySelectorAll('th')].findIndex(h=>h.dataset.col===col);return idx>-1?r.cells[idx].textContent:'';}switch(col){case 'size':return r.querySelector('td.col-size').getAttribute('title');case 'files':return r.querySelector('td.col-files').getAttribute('title');case 'pfb':return r.querySelector('td.col-pfb').textContent;case 'passpct':return r.querySelector('td.col-passpct').textContent;case 'run_id':return r.querySelector('td.col-run_id').textContent;case 'utc':return r.querySelector('td.col-utc').textContent;case 'context':return r.querySelector('td.col-context').textContent;case 'tags':return r.querySelector('td.col-tags').textContent;default:return r.textContent;}}"
|
106
|
+
"let sortState=null;"
|
107
|
+
"function sortBy(th){const col=th.dataset.col;const tbody=tbl.tBodies[0];const rows=[...tbody.rows];let dir=1;if(sortState&&sortState.col===col){dir=-sortState.dir;}sortState={col,dir};const numeric=(col==='size'||col==='files');rows.sort((r1,r2)=>{const a=extract(r1,col);const b=extract(r2,col);if(numeric){return ((Number(a)||0)-(Number(b)||0))*dir;}return a.localeCompare(b)*dir;});rows.forEach(r=>tbody.appendChild(r));tbl.tHead.querySelectorAll('th.sortable').forEach(h=>h.removeAttribute('data-sort'));th.setAttribute('data-sort',dir===1?'asc':'desc');updateAriaSort();lsSet('sort_col',col);lsSet('sort_dir',String(dir));}"
|
108
|
+
"tbl.tHead.querySelectorAll('th.sortable').forEach(th=>{th.addEventListener('click',()=>sortBy(th));});"
|
109
|
+
"function updateAriaSort(){tbl.tHead.querySelectorAll('th.sortable').forEach(th=>{th.setAttribute('aria-sort','none');});if(sortState){const th=tbl.tHead.querySelector(`th[data-col='${sortState.col}']`);if(th)th.setAttribute('aria-sort',sortState.dir===1?'ascending':'descending');}}" # noqa: E501
|
110
|
+
)
|
111
|
+
# Ensure sortBy calls updateAriaSort: simple append inside closure
|
112
|
+
if "function sortBy(th)" in RUNS_INDEX_JS:
|
113
|
+
RUNS_INDEX_JS = RUNS_INDEX_JS.replace(
|
114
|
+
"rows.forEach(r=>tbody.appendChild(r));tbl.tHead.querySelectorAll('th.sortable').forEach(h=>h.removeAttribute('data-sort'));th.setAttribute('data-sort',dir===1?'asc':'desc');lsSet('sort_col',col);lsSet('sort_dir',String(dir));}",
|
115
|
+
"rows.forEach(r=>tbody.appendChild(r));tbl.tHead.querySelectorAll('th.sortable').forEach(h=>h.removeAttribute('data-sort'));th.setAttribute('data-sort',dir===1?'asc':'desc');updateAriaSort();lsSet('sort_col',col);lsSet('sort_dir',String(dir));}",
|
116
|
+
)
|
117
|
+
|
118
|
+
# Sentinel substrings used in tests to verify template inclusion
|
119
|
+
RUNS_INDEX_SENTINELS = [
|
120
|
+
"ah_runs_",
|
121
|
+
"col-toggle",
|
122
|
+
"function applyFilter()",
|
123
|
+
]
|
124
|
+
|
125
|
+
# Post-bootstrap JS enhancements: aria-sort hook exposure, filter+URL glue,
|
126
|
+
# density persistence (already partly handled), failing-run notice, and
|
127
|
+
# in-progress marking using data-end-iso absence (v1 contract compliance).
|
128
|
+
RUNS_INDEX_JS_ENH = (
|
129
|
+
"document.addEventListener('DOMContentLoaded',()=>{"
|
130
|
+
"const table=document.getElementById('runs-table');if(!table)return;"
|
131
|
+
"if(localStorage.getItem('runs.dense')==='1'){table.classList.add('dense');}"
|
132
|
+
"table.querySelectorAll('tbody tr[data-v=\"1\"]').forEach(tr=>{if(!tr.dataset.endIso){tr.classList.add('run--inprogress');}});"
|
133
|
+
"window.setAriaSort=function(idx,dir){table.querySelectorAll('thead th').forEach((th,i)=>th.setAttribute('aria-sort',i===idx?dir:'none'));};"
|
134
|
+
"function setQS(k,v){const q=new URLSearchParams(location.search);if(v){q.set(k,v);}else{q.delete(k);}history.replaceState(null,'','?'+q);}"
|
135
|
+
"function applyFilters(){const q=new URLSearchParams(location.search);const gi=id=>document.getElementById(id);const branch=(gi('f-branch')?gi('f-branch').value.trim():q.get('branch')||'');const tagsStr=(gi('f-tags')?gi('f-tags').value.trim():q.get('tags')||'');const tags=tagsStr.split(',').filter(Boolean);const from=(gi('f-from')?gi('f-from').value:q.get('from'));const to=(gi('f-to')?gi('f-to').value:q.get('to'));let failing=(gi('f-onlyFailing')?gi('f-onlyFailing').checked:(q.get('onlyFailing')==='1')),anyFail=false;const fromEpoch=from?Date.parse(from+'T00:00:00Z')/1000:0;const toEpoch=to?(Date.parse(to+'T23:59:59Z')/1000):0;table.querySelectorAll('tbody tr[data-v=\"1\"]').forEach(tr=>{const rowBr=(tr.dataset.branch||'');const okBranch=!branch||rowBr===branch||rowBr.indexOf(branch)>=0;let rowTags=[];try{rowTags=JSON.parse(tr.dataset.tags||'[]');}catch(e){}const okTags=!tags.length||tags.every(t=>rowTags.includes(t));const epoch=parseInt(tr.dataset.epoch||'0',10);const okFrom=!from|| (epoch && epoch>=fromEpoch);const okTo=!to|| (epoch && epoch<=toEpoch);const isFail=(parseInt(tr.dataset.f||'0',10)>0);if(isFail)anyFail=true;const okFail=!failing||isFail;tr.hidden=!(okBranch&&okTags&&okFrom&&okTo&&okFail);});if(failing&&!anyFail){const q2=new URLSearchParams(location.search);q2.delete('onlyFailing');history.replaceState(null,'','?'+q2);let n=document.getElementById('notice');if(!n){n=document.createElement('div');n.id='notice';document.body.insertBefore(n,document.body.firstChild);}n.setAttribute('role','status');n.textContent='No failing runs — filter cleared.';}}"
|
136
|
+
"window.applyFilters=applyFilters;applyFilters();window._setQSFilter=setQS;"
|
137
|
+
"});"
|
138
|
+
)
|
139
|
+
|
140
|
+
# ---------------------------- Dashboard / Summary JS & CSS ----------------------------
|
141
|
+
# Consolidated summary cards + empty state + toggle logic, extracted from inline string.
|
142
|
+
RUNS_INDEX_DASHBOARD_CSS = (
|
143
|
+
".sc-toggle{margin:.25rem 0;padding:.25rem .5rem;border:1px solid var(--border,#2b2b2b);background:var(--bg-alt);border-radius:.5rem;cursor:pointer;font-size:12px}"
|
144
|
+
"#summary-cards .v.ok{color:#0a7a0a}#summary-cards .v.warn{color:#b8860b}#summary-cards .v.bad{color:#b00020}"
|
145
|
+
".empty{margin:.5rem 0;padding:.6rem .8rem;border:1px solid var(--border,#2b2b2b);border-radius:.5rem;background:var(--bg-alt);opacity:.85;font-size:12px}"
|
146
|
+
)
|
147
|
+
|
148
|
+
RUNS_INDEX_DASHBOARD_JS = (
|
149
|
+
"(function(){const tbl=document.getElementById('runs-table');const cards=document.getElementById('summary-cards');if(!tbl||!cards)return;"
|
150
|
+
"if(!document.getElementById('sc-span')){const spanCard=document.createElement('div');spanCard.className='card';spanCard.innerHTML='<div class=\"k\">Time span</div><div class=\"v\" id=\"sc-span\">—</div>';cards.appendChild(spanCard);}"
|
151
|
+
"const empty=document.getElementById('empty-msg');const toggle=document.getElementById('summary-toggle');"
|
152
|
+
"function visibleRows(){return [...tbl.querySelectorAll('tbody tr[data-v]')].filter(r=>!(r.hidden||getComputedStyle(r).display==='none'));}"
|
153
|
+
"function fmtPct(n){return isFinite(n)?(Math.round(n*10)/10).toFixed(1)+'%':'—';}"
|
154
|
+
r"function update(){const rows=visibleRows();if(empty)empty.hidden=rows.length!==0;const passEl=document.getElementById('sc-pass');const failEl=document.getElementById('sc-fail');const countEl=document.getElementById('sc-count');const latestEl=document.getElementById('sc-latest');let p=0,f=0,b=0,latestIso=null,latestId='—';rows.forEach(r=>{p+=+r.dataset.p||0;f+=+r.dataset.f||0;b+=+r.dataset.b||0;const iso=r.dataset.startIso||r.querySelector('[data-iso]')?.getAttribute('data-iso');if(iso&&(!latestIso||iso>latestIso)){latestIso=iso;latestId=r.dataset.runId||r.getAttribute('data-run-id')||'—';}});if(rows.length===0){[passEl,failEl,countEl,latestEl].forEach(el=>el&&(el.textContent='—'));const span=document.getElementById('sc-span');if(span)span.textContent='—';return;}const total=p+f+b;passEl&&(passEl.textContent=total?fmtPct(p/total*100):'—',passEl.classList.remove('ok','warn','bad'),(()=>{const num=parseFloat(passEl.textContent)||NaN;if(!isNaN(num))passEl.classList.add(num>=90?'ok':(num>=75?'warn':'bad'));})());failEl&&(failEl.textContent=String(f));countEl&&(countEl.textContent=String(rows.length));if(latestEl){if(latestIso){const base=location.pathname.replace(/runs/index\.html.*/,'');latestEl.innerHTML='<a href=\"'+base+latestId+'/\" title=\"Open latest run\">'+latestId+'</a>'; }else{latestEl.textContent=latestId;}}const span=document.getElementById('sc-span');if(span){const isoVals=rows.map(r=>r.dataset.startIso||'').filter(Boolean).sort();if(isoVals.length){const fmt=iso=>{try{return new Date(iso).toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'});}catch(e){return iso;}};span.textContent=fmt(isoVals[0])+' → '+fmt(isoVals[isoVals.length-1]);}}}"
|
155
|
+
"const orig=window.applyFilters;window.applyFilters=function(){orig&&orig();update();};document.addEventListener('DOMContentLoaded',update);update();"
|
156
|
+
"if(toggle){const key='runs.summary.collapsed';function setC(c){cards.hidden=c;toggle.setAttribute('aria-expanded',String(!c));toggle.textContent=c?'Summary ▶':'Summary ▼';try{localStorage.setItem(key,c?'1':'0');}catch(e){}}toggle.addEventListener('click',()=>setC(!cards.hidden));setC((()=>{try{return localStorage.getItem(key)==='1';}catch(e){return false;}})());}"
|
157
|
+
"})();"
|
158
|
+
)
|
pytest_allure_host/utils.py
CHANGED
@@ -31,6 +31,10 @@ def cache_control_for_key(key: str) -> str:
|
|
31
31
|
# widgets optional no-cache could be configurable later
|
32
32
|
if "/widgets/" in key:
|
33
33
|
return "no-cache"
|
34
|
+
# History JSON (e.g., latest/history/history-trend.json) changes frequently
|
35
|
+
# and should not be cached aggressively to ensure trend views refresh.
|
36
|
+
if "/history/" in key and key.endswith(".json"):
|
37
|
+
return "no-cache"
|
34
38
|
return "public, max-age=31536000, immutable"
|
35
39
|
|
36
40
|
|
@@ -58,6 +62,15 @@ class PublishConfig:
|
|
58
62
|
sse_kms_key_id: str | None = None
|
59
63
|
# arbitrary metadata (jira ticket, environment, etc.)
|
60
64
|
metadata: dict | None = None
|
65
|
+
# performance tuning
|
66
|
+
upload_workers: int | None = None # parallel upload threads
|
67
|
+
copy_workers: int | None = None # parallel copy threads
|
68
|
+
# optional archive artifact (compressed run bundle)
|
69
|
+
archive_run: bool | None = None
|
70
|
+
archive_format: str | None = None # 'tar.gz' or 'zip'
|
71
|
+
# UI feature toggles
|
72
|
+
# allow disabling summary cards dashboard if desired
|
73
|
+
summary_cards: bool = True
|
61
74
|
|
62
75
|
@property
|
63
76
|
def s3_run_prefix(self) -> str:
|
@@ -79,6 +92,17 @@ class PublishConfig:
|
|
79
92
|
)
|
80
93
|
return keys["latest"]
|
81
94
|
|
95
|
+
@property
|
96
|
+
def s3_latest_prefix_tmp(self) -> str:
|
97
|
+
"""Temporary staging prefix used during two-phase latest promotion.
|
98
|
+
|
99
|
+
Mirrors publish(): objects are first copied into latest_tmp/ then a
|
100
|
+
delete + copy sequence promotes them into latest/. Exposed so
|
101
|
+
plan_dry_run can surface this path for CI planning & tests.
|
102
|
+
"""
|
103
|
+
root = f"{self.prefix.rstrip('/')}/{self.project}/{self.branch}"
|
104
|
+
return f"{root}/latest_tmp/"
|
105
|
+
|
82
106
|
def url_run(self) -> str | None:
|
83
107
|
if not self.cloudfront_domain:
|
84
108
|
return None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pytest-allure-host
|
3
|
-
Version: 0.
|
3
|
+
Version: 2.0.0
|
4
4
|
Summary: Publish Allure static reports to private S3 behind CloudFront with history preservation
|
5
5
|
License-Expression: MIT
|
6
6
|
License-File: LICENSE
|
@@ -17,6 +17,7 @@ Classifier: Intended Audience :: Developers
|
|
17
17
|
Classifier: Topic :: Software Development :: Testing
|
18
18
|
Classifier: Framework :: Pytest
|
19
19
|
Classifier: Development Status :: 3 - Alpha
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
20
21
|
Classifier: Operating System :: OS Independent
|
21
22
|
Requires-Dist: PyYAML (>=6,<7)
|
22
23
|
Requires-Dist: boto3 (>=1.28,<2.0)
|
@@ -34,9 +35,12 @@ Description-Content-Type: text/markdown
|
|
34
35
|

|
35
36
|

|
36
37
|
[](https://darrenrabbs.github.io/allurehosting/)
|
38
|
+
[](https://github.com/darrenrabbs/allurehosting-cdk)
|
37
39
|
|
38
40
|
Publish Allure static reports to private S3 behind CloudFront with history preservation and SPA-friendly routing.
|
39
41
|
|
42
|
+
Optional infrastructure (AWS CDK stack to provision the private S3 bucket + CloudFront OAC distribution) lives externally: https://github.com/darrenrabbs/allurehosting-cdk
|
43
|
+
|
40
44
|
See `docs/architecture.md` and `.github/copilot-instructions.md` for architecture and design constraints.
|
41
45
|
|
42
46
|
## Documentation
|
@@ -63,6 +67,39 @@ The README intentionally stays lean—refer to the site for detailed guidance.
|
|
63
67
|
- Columns: Run ID, raw epoch, UTC Time (human readable), Size (pretty units), P/F/B (passed/failed/broken counts), links to the immutable run and the moving latest
|
64
68
|
- Newest run highlighted with a star (★) and soft background
|
65
69
|
|
70
|
+
## Quick start
|
71
|
+
|
72
|
+
```bash
|
73
|
+
# Install the publisher
|
74
|
+
pip install pytest-allure-host
|
75
|
+
|
76
|
+
# Run your test suite and produce allure-results/
|
77
|
+
pytest --alluredir=allure-results
|
78
|
+
|
79
|
+
# Plan (no uploads) – shows what would be published
|
80
|
+
publish-allure \
|
81
|
+
--bucket my-allure-bucket \
|
82
|
+
--project myproj \
|
83
|
+
--branch main \
|
84
|
+
--dry-run --summary-json plan.json
|
85
|
+
|
86
|
+
# Real publish (requires AWS creds: env vars, profile, or OIDC)
|
87
|
+
publish-allure \
|
88
|
+
--bucket my-allure-bucket \
|
89
|
+
--project myproj \
|
90
|
+
--branch main
|
91
|
+
```
|
92
|
+
|
93
|
+
Notes:
|
94
|
+
|
95
|
+
- `--prefix` defaults to `reports`; omit unless you need a different root.
|
96
|
+
- `--branch` defaults to `$GIT_BRANCH` or `main` if unset.
|
97
|
+
- Add `--cloudfront https://reports.example.com` to print CDN URLs.
|
98
|
+
- Use `--check` to preflight (AWS / allure binary / inputs) before a real run.
|
99
|
+
- Add `--context-url https://jira.example.com/browse/PROJ-123` to link a change ticket in the runs index.
|
100
|
+
- Use `--dry-run` + `--summary-json` in CI for a planning stage artifact.
|
101
|
+
- Provide `--ttl-days` and/or `--max-keep-runs` for lifecycle & cost controls.
|
102
|
+
|
66
103
|
## Requirements
|
67
104
|
|
68
105
|
- Python 3.9+
|
@@ -230,6 +267,56 @@ Pytest-driven (plugin):
|
|
230
267
|
--allure-max-keep-runs 10
|
231
268
|
```
|
232
269
|
|
270
|
+
### Minimal publish-only workflow
|
271
|
+
|
272
|
+
Create `.github/workflows/allure-publish.yml` for a lightweight pipeline that runs tests, generates the report, and publishes it (using secrets for the bucket and AWS credentials):
|
273
|
+
|
274
|
+
```yaml
|
275
|
+
name: allure-publish
|
276
|
+
on: [push, pull_request]
|
277
|
+
jobs:
|
278
|
+
publish:
|
279
|
+
runs-on: ubuntu-latest
|
280
|
+
permissions:
|
281
|
+
contents: read
|
282
|
+
steps:
|
283
|
+
- uses: actions/checkout@v4
|
284
|
+
- uses: actions/setup-python@v5
|
285
|
+
with:
|
286
|
+
python-version: "3.11"
|
287
|
+
- name: Install deps (minimal)
|
288
|
+
run: pip install pytest pytest-allure-host allure-pytest
|
289
|
+
- name: Run tests
|
290
|
+
run: pytest --alluredir=allure-results -q
|
291
|
+
- name: Publish Allure report (dry-run on PRs)
|
292
|
+
env:
|
293
|
+
AWS_REGION: us-east-1
|
294
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
295
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
296
|
+
ALLURE_BUCKET: ${{ secrets.ALLURE_BUCKET }}
|
297
|
+
run: |
|
298
|
+
EXTRA=""
|
299
|
+
if [ "${{ github.event_name }}" = "pull_request" ]; then EXTRA="--dry-run"; fi
|
300
|
+
publish-allure \
|
301
|
+
--bucket "$ALLURE_BUCKET" \
|
302
|
+
--project myproj \
|
303
|
+
--branch "${{ github.ref_name }}" \
|
304
|
+
--summary-json summary.json $EXTRA
|
305
|
+
- name: Upload publish summary (always)
|
306
|
+
if: always()
|
307
|
+
uses: actions/upload-artifact@v4
|
308
|
+
with:
|
309
|
+
name: allure-summary
|
310
|
+
path: summary.json
|
311
|
+
```
|
312
|
+
|
313
|
+
Notes:
|
314
|
+
|
315
|
+
- Add `--cloudfront https://reports.example.com` if you have a CDN domain.
|
316
|
+
- Add `--context-url ${{ github.server_url }}/${{ github.repository }}/pull/${{ github.event.pull_request.number }}` inside PRs to link the run to its PR.
|
317
|
+
- Use `--max-keep-runs` / `--ttl-days` to manage storage costs.
|
318
|
+
- For LocalStack-based tests, set `--s3-endpoint` and export `ALLURE_S3_ENDPOINT` in `env:`.
|
319
|
+
|
233
320
|
## Troubleshooting
|
234
321
|
|
235
322
|
- Missing Allure binary: ensure the Allure CLI is installed and on PATH.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
pytest_allure_host/__init__.py,sha256=v0peulTWwWlsJ0TpKgrjQx79plmqPp3pCOJyWJHAJPk,366
|
2
|
+
pytest_allure_host/__main__.py,sha256=1dzo3_74YYjXLo4xf_OjbmayOAzDdTlIgCVX00tHdoU,99
|
3
|
+
pytest_allure_host/cli.py,sha256=43wnhhcerl20fJcfQbtWIuR7CDHI--V1oIVpwaoV_PI,10235
|
4
|
+
pytest_allure_host/config.py,sha256=QLWhSYemmyI7cu_Y9ToR51hHwdU2lLLGqYT6IG9d1OE,4827
|
5
|
+
pytest_allure_host/plugin.py,sha256=t_DzPQAf3Vj8W9Xmt25__iZg7ItRdJzl7tyQu01ZtSI,5079
|
6
|
+
pytest_allure_host/publisher.py,sha256=c-QPZyDwK1c_e6QuYXLAF2uYXIdVMj8yVGwbP1snwUA,92763
|
7
|
+
pytest_allure_host/templates.py,sha256=tewywUUzhoLNayukhw-tOrSGtacMYSojzBKxE-cfRJE,18960
|
8
|
+
pytest_allure_host/utils.py,sha256=rRgvPwfHX2cviczjTkPpQ1SDpsXS8xLvQaVVAdZegzo,4449
|
9
|
+
pytest_allure_host-2.0.0.dist-info/METADATA,sha256=1W4JGqEbIbLqayYKsbytYVjZZNNg_6ND-saVp5YP8ZA,13621
|
10
|
+
pytest_allure_host-2.0.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
11
|
+
pytest_allure_host-2.0.0.dist-info/entry_points.txt,sha256=PWnSY4aqAumEX4-dc1TkNHfju5VTKPUHfYPv1SdAlIA,119
|
12
|
+
pytest_allure_host-2.0.0.dist-info/licenses/LICENSE,sha256=jcHN7r3njxuM4ilSLkK0fuuQAGMMkqzvYL27CYZGnWs,1084
|
13
|
+
pytest_allure_host-2.0.0.dist-info/RECORD,,
|
@@ -1,12 +0,0 @@
|
|
1
|
-
pytest_allure_host/__init__.py,sha256=Us3L46Kd1Rh1FM9e74PJB7TN4G37WkM12edi8GnGhgc,130
|
2
|
-
pytest_allure_host/__main__.py,sha256=1dzo3_74YYjXLo4xf_OjbmayOAzDdTlIgCVX00tHdoU,99
|
3
|
-
pytest_allure_host/cli.py,sha256=OLizf1zTZ4yeppg1dcv9QYqJBwoa1FsXUoBgJAmd_o8,4932
|
4
|
-
pytest_allure_host/config.py,sha256=QLWhSYemmyI7cu_Y9ToR51hHwdU2lLLGqYT6IG9d1OE,4827
|
5
|
-
pytest_allure_host/plugin.py,sha256=t_DzPQAf3Vj8W9Xmt25__iZg7ItRdJzl7tyQu01ZtSI,5079
|
6
|
-
pytest_allure_host/publisher.py,sha256=0qTaJW5gFIbjM26uyn5b49gqqTJWbxgWvCt7PCnhQfA,32691
|
7
|
-
pytest_allure_host/utils.py,sha256=oZm3RqhTYMsyf-3IN4ugFLK3gEz9jQ_rmdb2BeQoYBU,3329
|
8
|
-
pytest_allure_host-0.1.2.dist-info/METADATA,sha256=MyVVqcfrynwG4j4YkztkGRNBRFgq_PNPGGSxF5FJSj8,10387
|
9
|
-
pytest_allure_host-0.1.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
10
|
-
pytest_allure_host-0.1.2.dist-info/entry_points.txt,sha256=PWnSY4aqAumEX4-dc1TkNHfju5VTKPUHfYPv1SdAlIA,119
|
11
|
-
pytest_allure_host-0.1.2.dist-info/licenses/LICENSE,sha256=jcHN7r3njxuM4ilSLkK0fuuQAGMMkqzvYL27CYZGnWs,1084
|
12
|
-
pytest_allure_host-0.1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|