#!/usr/bin/env python3 """Test the candidate in isolated project-local and personal TEXMF layouts.""" from pathlib import Path import argparse import json import os import re import shutil import subprocess import tempfile import time PKG = Path(__file__).resolve().parents[2] WORK = PKG.parent REPORTS = WORK / 'reports' def copy_runtime(target): for p in (PKG/'tex').iterdir(): shutil.copyfile(p, target/p.name) shutil.copyfile(PKG/'fonts/OpenGlyphEbag.otf', target/'OpenGlyphEbag.otf') def run(engine, args, cwd, env, limit=120): started = time.monotonic() p = subprocess.run([engine, *args], cwd=cwd, env=env, text=True, capture_output=True, timeout=limit) return p, time.monotonic()-started def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument('--engine', choices=['tectonic','xelatex','lualatex'], required=True) ap.add_argument('--manual', action='store_true') ap.add_argument('--publish-pdf', action='store_true') args = ap.parse_args() if not shutil.which(args.engine): raise SystemExit('Missing engine: '+args.engine) REPORTS.mkdir(exist_ok=True) (WORK/'build').mkdir(exist_ok=True) results = [] fixtures = { 'mixed': (True, None, r'\OpenGlyphLoad{custom}\begin{document}Text \gwglyph{my:n1}, \gwglyph{my:n2}.\footnote{\gwglyph{my:n3}}\section{Heading \gwglyph{my:n4}}\end{document}'), 'repeat-load': (True, None, r'\OpenGlyphLoad{custom}\OpenGlyphLoad{custom}\OpenGlyphAlias{short}{my:n1}\begin{document}\gwglyph{short}\OpenGlyphSequence{my:n2,my:n3}\end{document}'), 'unknown': (False, 'Unknown glyph', r'\OpenGlyphLoad{custom}\begin{document}\gwglyph{missing}\end{document}'), 'duplicate': (False, 'already defined', r'\OpenGlyphLoad{custom}\OpenGlyphAlias{my:n1}{my:n2}\begin{document}x\end{document}'), 'missing-font': (False, 'Cannot find', r'\OpenGlyphDeclareFont{bad}{no-such-font.otf}\begin{document}x\end{document}'), 'missing-cmap': (False, 'does not contain glyph', r'\OpenGlyphLoad{custom}\OpenGlyphDeclare{bad}{my}{0041}\begin{document}\gwglyph{bad}\end{document}'), 'bad-id': (False, 'Invalid ASCII identifier', r'\OpenGlyphLoad{custom}\OpenGlyphAlias{bad/id}{my:n1}\begin{document}x\end{document}'), 'existence': (True, None, r'\OpenGlyphLoad{custom}\begin{document}\OpenGlyphIfExistsTF{my:n1}{YES}{\errmessage{miss}}\OpenGlyphIfExistsTF{missing}{\errmessage{unexpected}}{NO}\end{document}'), 'namespaces': (True, None, r'\OpenGlyphLoad{custom}\OpenGlyphLoad{second}\begin{document}\gwglyph{my:n1}\gwglyph{second:n1}\end{document}'), 'builtin': (True, None, r'\OpenGlyphLoad{ebag}\OpenGlyphLoad{ebag}\begin{document}\ebag{s030-067}\ebag{s131-009}\ebag{s162-428}\end{document}'), 'builtin-and-custom': (True, None, r'\OpenGlyphLoad{ebag}\OpenGlyphLoad{custom}\begin{document}\ebag{s030-067}\gwglyph{my:n2}\end{document}'), 'builtin-sizes': (True, None, r'\OpenGlyphLoad{ebag}\begin{document}\ebag{s030-067} {\Huge\ebag{s030-067}}\footnote{\ebag{s030-067}}\end{document}'), 'missing-local-font-no-fallback': (False, 'Cannot find', r'\OpenGlyphLoad{broken}\begin{document}x\end{document}'), 'path-with-spaces': (True, None, r'\OpenGlyphLoad{custom bundle}\begin{document}\gwglyph{spaces:n1}\end{document}'), } with tempfile.TemporaryDirectory(prefix=args.engine+'-', dir=WORK/'build') as tmp: base=Path(tmp);paper=base/'unrelated paper';paper.mkdir() env=os.environ.copy() for key in ('TEXINPUTS','OPENTYPEFONTS','TEXMFHOME','TEXMFVAR','TEXMFCONFIG','TEXMFOUTPUT'): env.pop(key, None) env['PYTHONDONTWRITEBYTECODE']='1' layout='project-local' if args.engine=='tectonic': copy_runtime(paper) else: layout='personal-TEXMF' texmf=base/'texmf' texdir=texmf/'tex/latex/openglyph';texdir.mkdir(parents=True) fontdir=texmf/'fonts/opentype/public/openglyph';fontdir.mkdir(parents=True) for p in (PKG/'tex').iterdir():shutil.copyfile(p,texdir/p.name) shutil.copyfile(PKG/'fonts/OpenGlyphEbag.otf',fontdir/'OpenGlyphEbag.otf') env.update(TEXMFHOME=str(texmf),TEXMFVAR=str(base/'texmf-var'),TEXMFCONFIG=str(base/'texmf-config')) for filename,extra in [('openglyph.sty',[]),('openglyph-ebag.def',[]),('OpenGlyphEbag.otf',['--format=opentype fonts'])]: p,_=run('kpsewhich',[*extra,filename],paper,env) got=p.stdout.strip() results.append({'case':'lookup-'+filename,'layout':layout,'passed':p.returncode==0 and got.startswith(str(texmf)+'/'),'resolved':got}) for name,namespace in [('custom','my'),('second','second'),('custom bundle','spaces')]: shutil.copytree(PKG/'doc/examples/myglyphs',paper/name) catalog=paper/name/'catalog.tex' if namespace!='my':catalog.write_text(catalog.read_text().replace('{my}','{'+namespace+'}').replace('{my:','{'+namespace+':')) # This missing local font must not silently pick the installed built-in. (paper/'broken').mkdir() (paper/'broken/catalog.tex').write_text(r'\OpenGlyphDeclareFont{broken}{OpenGlyphEbag.otf}') # Original local ebag/catalog.tex layout still has priority over TEXMF. shutil.copytree(PKG/'doc/examples/myglyphs',paper/'legacy') legacy=(paper/'legacy/catalog.tex').read_text().replace('{my}','{ebag}').replace('{my:','{ebag:') (paper/'legacy/catalog.tex').write_text(legacy) def compile_one(case,source,success=True,error=None,cwd=paper): tex=cwd/(case+'.tex');tex.write_text(source,encoding='utf-8') flags=['--keep-logs',tex.name] if args.engine=='tectonic' else ['-no-shell-escape','-interaction=nonstopmode','-halt-on-error',tex.name] p,seconds=run(args.engine,flags,cwd,env) output=p.stdout+p.stderr log=cwd/(case+'.log') if log.exists():output+='\n'+log.read_text(errors='replace') diagnostic=re.sub(r'\s+',' ',output) passed=(p.returncode==0)==success and (not error or error in diagnostic) if success and 'Missing character:' in output:passed=False if case in ('openglyph-doc','openglyph-example') and 'Overfull' in output:passed=False (REPORTS/(args.engine+'-'+case+'.log')).write_text(output,encoding='utf-8') results.append(dict(case=case,layout=layout,expected_success=success,exit_code=p.returncode,passed=passed,seconds=round(seconds,3),expected_error=error)) print(json.dumps(results[-1]),flush=True) return passed for case,(success,error,body) in fixtures.items(): compile_one(case,r'\documentclass{article}\usepackage{openglyph}\usepackage[unicode]{hyperref}'+body,success,error) (paper/'legacy').rename(paper/'ebag') compile_one('legacy-local-priority',r'\documentclass{article}\usepackage{openglyph}\OpenGlyphLoad{ebag}\begin{document}\ebag{n1}\OpenGlyphIfExistsTF{ebag:s030-067}{\errmessage{wrong catalogue}}{}\end{document}') if args.engine!='tectonic': # Explicit absolute custom bundle paths also work, including spaces. moved=base/'lookup area';moved.mkdir() shutil.copytree(PKG/'doc/examples/myglyphs',moved/'bysearch') compile_one('absolute-catalogue-font-relative',r'\documentclass{article}\usepackage{openglyph}\OpenGlyphLoad{'+(moved/'bysearch').as_posix()+r'}\begin{document}\gwglyph{my:n1}\end{document}') if args.manual: doc=base/'doc';shutil.copytree(PKG/'doc',doc) shutil.copytree(PKG/'source/examples',base/'source/examples') if args.engine=='tectonic':copy_runtime(doc) for stem in ('openglyph-doc','openglyph-example'): source=(doc/(stem+'.tex')).read_text() good=True for attempt in (1,2): good=compile_one(stem,source,cwd=doc) and good if good and args.publish_pdf: shutil.copyfile(doc/(stem+'.pdf'),PKG/'doc'/(stem+'.pdf')) version=subprocess.check_output([args.engine,'--version'],text=True,timeout=15).splitlines()[0] result={'engine':args.engine,'engine_version':version,'layout':layout,'shell_escape':False,'tests':results, 'all_passed':all(x['passed'] for x in results)} (REPORTS/('tex-'+args.engine+'.json')).write_text(json.dumps(result,indent=2)+'\n') if not result['all_passed']:raise SystemExit(1) if __name__=='__main__':main()