diff --git a/RFCs/0027-cli-check-mode.md b/RFCs/0027-cli-check-mode.md index f7feeb0..07f0a3f 100644 --- a/RFCs/0027-cli-check-mode.md +++ b/RFCs/0027-cli-check-mode.md @@ -4,7 +4,7 @@ ## 决议 -`qcoffee --check FILE`(FILE 可为 `-`)读取源码,执行词法、解析、编译与 `Chunk::verify`,但绝不创建执行 Context 或运行字节码。成功时无标准输出并以零退出;读取、语法、编译或验证错误以非零退出。它不能与 `--dump-bytecode` 组合。 +`qcoffee --check FILE`(FILE 可为 `-`)读取源码,执行词法、解析、编译与 `Chunk::verify`,但绝不创建执行 Context 或运行字节码。成功时无标准输出并以零退出;读取、语法、编译或验证错误以非零退出。它不能与 `--dump-bytecode` 组合,也不能与 `-e`、脚本路径或第二个源码模式重复指定输入。 该模式面向 CI、编辑器与文学文档流程:例如无限循环是有效程序,`qcoffee --fuel 1 --check program.qc` 必须成功,因为 fuel 仅限制执行。 diff --git a/RFCs/0079-qtest-tap-output.md b/RFCs/0079-qtest-tap-output.md index b7b8244..b2b8700 100644 --- a/RFCs/0079-qtest-tap-output.md +++ b/RFCs/0079-qtest-tap-output.md @@ -11,6 +11,8 @@ `qtest --tap FILE_OR_DIRECTORY...` 输出 TAP version 13。所有输入递归发现的文件合并后按路径全局排序并去重,然后每个文件恰输出一条编号记录:成功为 `ok N - path`,失败为 `not ok N - path`。失败详情紧随其后按 `# ` 注释行输出;详情中的换行分割为多条注释,避免伪造新的测试记录。末尾输出 `1..N`,其中 `N` 是去重后发现的测试文件数。 +递归目录时允许文件系统符号链接,但规范化后的目录路径只访问一次;自引用或环状链接被跳过,不得造成无限递归或栈耗尽。 + `--tap` 与 `--json` 互斥,冲突以用法错误退出码 2 结束。读取错误或找不到测试文件时,在已输出 TAP 版本后输出 `Bail out!` 并以非零状态退出。测试失败仍返回非零状态;`--stats` 继续只写标准错误,不污染 TAP 标准输出。 ## 验收 diff --git a/RFCs/0080-cli-bytecode-fingerprint.md b/RFCs/0080-cli-bytecode-fingerprint.md index d75bc86..127068c 100644 --- a/RFCs/0080-cli-bytecode-fingerprint.md +++ b/RFCs/0080-cli-bytecode-fingerprint.md @@ -11,7 +11,7 @@ `qcoffee --fingerprint FILE` 读取、编译并验证文件(`FILE` 可为 `-`),然后在标准输出输出一个不带前缀的 16 位小写十六进制 `u64` 和换行。它不执行脚本,不创建脚本全局状态,也不输出 fuel 统计。 -`--fingerprint` 与 `--check`、`--dump-bytecode`、`--interactive`、`--stats` 互斥;冲突或缺少文件参数为用法错误,退出码为 2。读取、解析或验证错误沿用普通 CLI 的非零退出与结构化错误文本。相同源码必须产生相同指纹,不同有效字节码内容应产生不同指纹(碰撞概率遵循 64 位指纹的通常边界)。 +`--fingerprint` 与 `--check`、`--dump-bytecode`、`--interactive`、`--stats` 互斥,且只能指定一个源码输入;冲突或缺少文件参数为用法错误,退出码为 2。读取、解析或验证错误沿用普通 CLI 的非零退出与结构化错误文本。相同源码必须产生相同指纹,不同有效字节码内容应产生不同指纹(碰撞概率遵循 64 位指纹的通常边界)。 ## 验收 diff --git a/docs/manual.classical-zh.html b/docs/manual.classical-zh.html index cd92eb1..aea453b 100644 --- a/docs/manual.classical-zh.html +++ b/docs/manual.classical-zh.html @@ -10,7 +10,7 @@

! 同 not,严反 Bool;!= 仍严不等。

严等与数较可相连,中项惟求一遍,前否则后不求。

qcoffee - 者,自标准输入读其文也。

-

qcoffee --stats 则书所试指令与余燃料于标准错误,程序之标准输出不改。

+

qcoffee --stats 则书所试指令与余燃料于标准错误,程序之标准输出不改;每次惟一源码输入,执行模式相冲则报用法之误。

qcoffee --check FILE 者,析编验其文而不行也。

qcoffee --interactive(或 -i)者,逐行共用一 Context;:help 示命,:quit 出之。

qcoffee --interactive --stats 惟非空行之行而行或运行时有误者,书指令与余燃料一条;析验之误不更书。

diff --git a/docs/manual.classical-zh.md b/docs/manual.classical-zh.md index 3c2ab20..cce4f91 100644 --- a/docs/manual.classical-zh.md +++ b/docs/manual.classical-zh.md @@ -17,7 +17,7 @@ QuickCoffee 者,Rust 所为字节码机也,非 JavaScript 之运行时。其 欲试之,曰:`qcoffee -e "print(range(1, 4))"`。`--check FILE` 者,析、编、验其文而不行;`--fuel N` 者,限所行指令之数也;数尽则止而报误。内府有 `print`、`len`、`type`、`range`、`str`、`keys`、`values`、`join`、`split`、`assert`;`range(a, b)` 取自 a 至 b 前。 `qcoffee -` 自标准输入读其文,便于管道;`qcoffee --dump-bytecode -` 则析其指令而不行之。 -`qcoffee --stats` 则书所试指令与余燃料于标准错误,程序之标准输出不改;不可与 `--check`、`--dump-bytecode` 并用。 +`qcoffee --stats` 则书所试指令与余燃料于标准错误,程序之标准输出不改;每次惟一源码输入,执行模式相冲则报用法之误。 `qcoffee --interactive`(或 `-i`)逐行共用一 Context;`:help` 示其命,`:quit`/`:exit` 出之。管道输入不见提示。 交互而加 `--stats`,每一非空行亦书所试指令与余燃料于标准错误。`for character, index in 'a☕中' then index` 得 `[0, 1, 2]`;字符串循 Unicode 标量,不受 `by`。`[head, tail...] = [1, 2, 3]`,tail 得 `[2, 3]`;数组之 rest 必居末。 diff --git a/docs/manual.devanagari-sa.html b/docs/manual.devanagari-sa.html index 858f80c..55f82a1 100644 --- a/docs/manual.devanagari-sa.html +++ b/docs/manual.devanagari-sa.html @@ -5,7 +5,7 @@

ऋण-सूचकाङ्केन क्रमस्य अन्तिमं पदं लभ्यते।

स्रोतः पठ्यते, सत्यापित-bytecode मध्ये संकल्यते, fuel-सीमया चालयते।

qcoffee - मानक-input तः QuickCoffee-program पठति।

-

qcoffee --stats instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति।

+

qcoffee --stats instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति; एकमेव source ग्राह्यः, विरोधि execution-mode तु usage-दोषं जनयति।

qcoffee --check FILE स्रोतं verify करोति, न चालयति।

qcoffee --interactive (वा -i) एकं Context पङ्क्ति-क्रमेण धारयति; :help दर्शयति, :quit निर्गच्छति।

qcoffee --interactive --stats केवलं कार्यितायै वा runtime-दोषयुक्तायै non-empty पङ्क्त्यै instruction तथा fuel लेखं लिखति; parse अथवा verify-दोषे नूतनं लेखं न लिखति।

diff --git a/docs/manual.devanagari.sa.md b/docs/manual.devanagari.sa.md index 6ec0982..2dc2466 100644 --- a/docs/manual.devanagari.sa.md +++ b/docs/manual.devanagari.sa.md @@ -17,7 +17,7 @@ strict अथवा numeric-comparison श्रृङ्खला भवति: `qcoffee -e "print(range(1, 4))"` प्रयुञ्जीत। `qcoffee --check FILE` स्रोतं compile-verify करोति, न चालयति; `--fuel N` निर्देश-संख्यां सीमयति। `print`, `len`, `type`, `range`, `str`, `keys`, `values`, `join`, `split`, `assert` मानक-सहायकाः; `range(a, b)` मध्ये `b` न गृह्यते। `qcoffee -` मानक-input तः स्रोतः पठति; `qcoffee --dump-bytecode -` तमेव स्रोतं चालनं विना विच्छिनत्ति। -`qcoffee --stats` instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति; `--check` अथवा `--dump-bytecode` सह न योज्यम्। +`qcoffee --stats` instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति; एकमेव source ग्राह्यः, विरोधि execution-mode तु usage-दोषं जनयति। `qcoffee --interactive` (वा `-i`) पङ्क्तिषु एकं Context धारयति; `:help` आदेशान् दर्शयति, `:quit`/`:exit` सत्रं समापयतः। pipe-input मध्ये prompt न भवति। `for character, index in 'a☕中' then index` `[0, 1, 2]` ददाति; string Unicode-scalar-क्रमेण चलति, `by` न स्वीकरोति। diff --git a/docs/manual.en.html b/docs/manual.en.html index 9e38307..e4393f4 100644 --- a/docs/manual.en.html +++ b/docs/manual.en.html @@ -2,7 +2,7 @@

Source is parsed, compiled to verified bytecode, and executed with a fuel budget.

qcoffee - reads a QuickCoffee program from standard input.

-

qcoffee --stats writes instruction and remaining-fuel counters to stderr while preserving program stdout.

+

qcoffee --stats writes instruction and remaining-fuel counters to stderr while preserving program stdout; qcoffee accepts one source input and rejects conflicting execution modes.

qcoffee --check FILE parses, compiles, and verifies without executing FILE.

qcoffee --interactive (or -i) keeps one Context for a line-oriented session; :help and :quit are built-in commands.

qcoffee --interactive --stats writes one instruction/fuel record for each non-empty line that executes or reaches a runtime error; parse and verify errors write none.

diff --git a/docs/manual.en.md b/docs/manual.en.md index df5287c..965f217 100644 --- a/docs/manual.en.md +++ b/docs/manual.en.md @@ -17,7 +17,7 @@ Adjacent strict or numeric comparisons may chain: `1 < middle() < 3` evaluates ` Run `qcoffee -e "print(range(1, 4))"`, `qcoffee --fuel 10000 program.qc`, `qcoffee --check program.qc`, or `qcoffee --dump-bytecode program.qc`. `--check` parses, compiles, and verifies without executing. Fuel limits executed instructions; exhaustion is a safe error. The small standard library contains `print`, `len`, `type`, end-exclusive `range(a, b)`, `str`, `keys`, `values`, `join`, `split`, and `assert`. `qcoffee -` reads source from standard input, which is convenient in pipelines; `qcoffee --dump-bytecode -` disassembles that same input instead of executing it. -`qcoffee --stats` writes instruction and remaining-fuel counters to stderr while preserving program stdout; it cannot be combined with `--check` or `--dump-bytecode`. +`qcoffee --stats` writes instruction and remaining-fuel counters to stderr while preserving program stdout; qcoffee accepts one source input and rejects conflicting execution modes. `qcoffee --interactive` (or `-i`) keeps one Context across input lines; `:help` lists commands and `:quit`/`:exit` leave the session. Piped input receives no prompts. With `--stats`, each non-empty interactive line that executes or reaches a runtime error writes its instruction and remaining-fuel counters to stderr; parse and verify errors write no fresh record. diff --git a/docs/manual.latin.html b/docs/manual.latin.html index 9f64d48..b6d43db 100644 --- a/docs/manual.latin.html +++ b/docs/manual.latin.html @@ -5,7 +5,7 @@

Indices negativi in seriebus et textu Unicode extremum elementum petunt.

Fons legitur, in bytecodicem verificatum compilatur, et cum limite fuel currit.

qcoffee - programma QuickCoffee ex initio normali legit.

-

qcoffee --stats numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet.

+

qcoffee --stats numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet; unus tantum fons admittitur et modi contrarii errorem usus reddunt.

qcoffee --check FILE fontem verificat sine cursu.

qcoffee --interactive (vel -i) unum Context per lineas servat; :help docet, :quit exit.

qcoffee --interactive --stats unam instructionum et alimenti reliqui notam lineae non vacuae exsecutæ vel errorem currendi ferenti scribit; errores analysi vel verificationis nihil scribunt.

diff --git a/docs/manual.latin.md b/docs/manual.latin.md index d07bc4c..8493d84 100644 --- a/docs/manual.latin.md +++ b/docs/manual.latin.md @@ -17,7 +17,7 @@ Comparationes strictae vel numericae conecti possunt: `1 < middle() < 3` medium Utere `qcoffee -e "print(range(1, 4))"`; `qcoffee --check FILE` fontem compilat atque verificat sine cursu; `--fuel N` numerum instructionum finit. Bibliotheca parva habet `print`, `len`, `type`, `range(a, b)`, `str`, `keys`, `values`, `join`, `split`, et `assert`; finis in `range` exclusus est. `qcoffee -` fontem ex initio normali legit; `qcoffee --dump-bytecode -` illum fontem sine cursu explicat. -`qcoffee --stats` numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet; cum `--check` aut `--dump-bytecode` coniungi non potest. +`qcoffee --stats` numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet; unus tantum fons admittitur et modi contrarii errorem usus reddunt. `qcoffee --interactive` (vel `-i`) unum Context inter lineas servat; `:help` imperia ostendit, `:quit`/`:exit` sessionem finiunt. Input per fistulam promptum non accipit. `for character, index in 'a☕中' then index` indices `[0, 1, 2]` reddit; stringae per scalas Unicode iterantur et `by` non admittunt. diff --git a/docs/manual.zh-CN.html b/docs/manual.zh-CN.html index 80b798c..ef8698f 100644 --- a/docs/manual.zh-CN.html +++ b/docs/manual.zh-CN.html @@ -2,7 +2,7 @@

QuickCoffee 先将源码解析并编译为经验证的字节码,随后由带 fuel 限制的 VM 执行。

qcoffee - 可从标准输入读取 QuickCoffee 程序。

-

qcoffee --stats 将指令数与剩余燃料写入标准错误,同时保持程序标准输出不变。

+

qcoffee --stats 将指令数与剩余燃料写入标准错误,同时保持程序标准输出不变;qcoffee 每次只接受一个源码输入,冲突执行模式会报用法错误。

qcoffee --check FILE 只解析、编译并验证 FILE,不执行它。

qcoffee --interactive(或 -i)逐行复用同一 Context;:help 显示命令,:quit 退出。

qcoffee --interactive --stats 仅为实际执行或运行时失败的非空输入行输出指令/燃料统计;解析、验证错误不输出新记录。

diff --git a/docs/manual.zh-CN.md b/docs/manual.zh-CN.md index 4d95143..2fd3e8e 100644 --- a/docs/manual.zh-CN.md +++ b/docs/manual.zh-CN.md @@ -22,7 +22,7 @@ qcoffee --dump-bytecode program.qc ``` `qcoffee -` 从标准输入读取源码,便于接入管道;`qcoffee --check FILE`(FILE 可为 `-`)只解析、编译和验证而不执行;`qcoffee --dump-bytecode -` 则反汇编标准输入而不执行。 -`qcoffee --stats` 将指令数与剩余 fuel 写入标准错误,同时保持程序标准输出不变;不可与 `--check` 或 `--dump-bytecode` 合用。 +`qcoffee --stats` 将指令数与剩余 fuel 写入标准错误,同时保持程序标准输出不变;qcoffee 每次只接受一个源码输入,冲突执行模式会报用法错误。 `qcoffee --interactive`(或 `-i`)在输入行之间保持同一 Context;`:help` 列出命令,`:quit`/`:exit` 退出会话。管道输入不输出提示符。 交互模式加 `--stats` 时,仅实际执行或运行时失败的非空输入行把指令数与剩余 fuel 写入标准错误;解析、验证错误不生成新记录。 diff --git a/manuals/manual.classical-zh.qc b/manuals/manual.classical-zh.qc index 0434150..0c2be39 100644 --- a/manuals/manual.classical-zh.qc +++ b/manuals/manual.classical-zh.qc @@ -10,7 +10,7 @@ ## ! 同 not,严反 Bool;!= 仍严不等。 ## 严等与数较可相连,中项惟求一遍,前否则后不求。 ## qcoffee - 者,自标准输入读其文也。 -## qcoffee --stats 则书所试指令与余燃料于标准错误,程序之标准输出不改。 +## qcoffee --stats 则书所试指令与余燃料于标准错误,程序之标准输出不改;每次惟一源码输入,执行模式相冲则报用法之误。 ## qcoffee --check FILE 者,析编验其文而不行也。 ## qcoffee --interactive(或 -i)者,逐行共用一 Context;:help 示命,:quit 出之。 ## qcoffee --interactive --stats 惟非空行之行而行或运行时有误者,书指令与余燃料一条;析验之误不更书。 diff --git a/manuals/manual.devanagari-sa.qc b/manuals/manual.devanagari-sa.qc index 86f9f44..3f3de48 100644 --- a/manuals/manual.devanagari-sa.qc +++ b/manuals/manual.devanagari-sa.qc @@ -5,7 +5,7 @@ ## ऋण-सूचकाङ्केन क्रमस्य अन्तिमं पदं लभ्यते। ## स्रोतः पठ्यते, सत्यापित-bytecode मध्ये संकल्यते, fuel-सीमया चालयते। ## qcoffee - मानक-input तः QuickCoffee-program पठति। -## qcoffee --stats instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति। +## qcoffee --stats instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति; एकमेव source ग्राह्यः, विरोधि execution-mode तु usage-दोषं जनयति। ## qcoffee --check FILE स्रोतं verify करोति, न चालयति। ## qcoffee --interactive (वा -i) एकं Context पङ्क्ति-क्रमेण धारयति; :help दर्शयति, :quit निर्गच्छति। ## qcoffee --interactive --stats केवलं कार्यितायै वा runtime-दोषयुक्तायै non-empty पङ्क्त्यै instruction तथा fuel लेखं लिखति; parse अथवा verify-दोषे नूतनं लेखं न लिखति। diff --git a/manuals/manual.en.qc b/manuals/manual.en.qc index 8644a70..96b55ea 100644 --- a/manuals/manual.en.qc +++ b/manuals/manual.en.qc @@ -2,7 +2,7 @@ ## ## Source is parsed, compiled to verified bytecode, and executed with a fuel budget. ## qcoffee - reads a QuickCoffee program from standard input. -## qcoffee --stats writes instruction and remaining-fuel counters to stderr while preserving program stdout. +## qcoffee --stats writes instruction and remaining-fuel counters to stderr while preserving program stdout; qcoffee accepts one source input and rejects conflicting execution modes. ## qcoffee --check FILE parses, compiles, and verifies without executing FILE. ## qcoffee --interactive (or -i) keeps one Context for a line-oriented session; :help and :quit are built-in commands. ## qcoffee --interactive --stats writes one instruction/fuel record for each non-empty line that executes or reaches a runtime error; parse and verify errors write none. diff --git a/manuals/manual.latin.qc b/manuals/manual.latin.qc index 6595508..e0dbdd0 100644 --- a/manuals/manual.latin.qc +++ b/manuals/manual.latin.qc @@ -5,7 +5,7 @@ ## Indices negativi in seriebus et textu Unicode extremum elementum petunt. ## Fons legitur, in bytecodicem verificatum compilatur, et cum limite fuel currit. ## qcoffee - programma QuickCoffee ex initio normali legit. -## qcoffee --stats numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet. +## qcoffee --stats numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet; unus tantum fons admittitur et modi contrarii errorem usus reddunt. ## qcoffee --check FILE fontem verificat sine cursu. ## qcoffee --interactive (vel -i) unum Context per lineas servat; :help docet, :quit exit. ## qcoffee --interactive --stats unam instructionum et alimenti reliqui notam lineae non vacuae exsecutæ vel errorem currendi ferenti scribit; errores analysi vel verificationis nihil scribunt. diff --git a/manuals/manual.zh-CN.qc b/manuals/manual.zh-CN.qc index ec09888..1f944d1 100644 --- a/manuals/manual.zh-CN.qc +++ b/manuals/manual.zh-CN.qc @@ -2,7 +2,7 @@ ## ## QuickCoffee 先将源码解析并编译为经验证的字节码,随后由带 fuel 限制的 VM 执行。 ## qcoffee - 可从标准输入读取 QuickCoffee 程序。 -## qcoffee --stats 将指令数与剩余燃料写入标准错误,同时保持程序标准输出不变。 +## qcoffee --stats 将指令数与剩余燃料写入标准错误,同时保持程序标准输出不变;qcoffee 每次只接受一个源码输入,冲突执行模式会报用法错误。 ## qcoffee --check FILE 只解析、编译并验证 FILE,不执行它。 ## qcoffee --interactive(或 -i)逐行复用同一 Context;:help 显示命令,:quit 退出。 ## qcoffee --interactive --stats 仅为实际执行或运行时失败的非空输入行输出指令/燃料统计;解析、验证错误不输出新记录。 diff --git a/src/bin/qtest.rs b/src/bin/qtest.rs index af60be0..3e24036 100644 --- a/src/bin/qtest.rs +++ b/src/bin/qtest.rs @@ -2,17 +2,28 @@ use quickcoffee::{Context, Value}; use std::{ + collections::HashSet, env, fs, path::{Path, PathBuf}, process::ExitCode, }; -fn collect(path: &Path, files: &mut Vec) -> Result<(), String> { +fn collect( + path: &Path, + files: &mut Vec, + visited_directories: &mut HashSet, +) -> Result<(), String> { let metadata = fs::metadata(path).map_err(|error| error.to_string())?; if metadata.is_file() { files.push(path.to_path_buf()); return Ok(()); } + if metadata.is_dir() { + let canonical = fs::canonicalize(path).map_err(|error| error.to_string())?; + if !visited_directories.insert(canonical) { + return Ok(()); + } + } let mut entries: Vec<_> = fs::read_dir(path) .map_err(|error| error.to_string())? .collect::, _>>() @@ -21,7 +32,7 @@ fn collect(path: &Path, files: &mut Vec) -> Result<(), String> { for entry in entries { let child = entry.path(); if child.is_dir() { - collect(&child, files)?; + collect(&child, files, visited_directories)?; } else if child.extension().is_some_and(|extension| extension == "qc") { files.push(child); } @@ -101,8 +112,9 @@ fn main() -> ExitCode { return ExitCode::from(2); } let mut files = vec![]; + let mut visited_directories = HashSet::new(); for input in inputs { - if let Err(error) = collect(Path::new(&input), &mut files) { + if let Err(error) = collect(Path::new(&input), &mut files, &mut visited_directories) { if tap { println!("TAP version 13"); println!("Bail out! {input}: {error}"); diff --git a/src/main.rs b/src/main.rs index 02db01f..ea36b0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ use std::{ process::ExitCode, }; +const EXECUTION_MODE_ALTERNATIVES: &str = "source inputs (-e SOURCE, FILE, and -), --check, --dump-bytecode, --fingerprint, and --stats are execution-mode alternatives"; + fn usage() { eprintln!( "Usage: qcoffee [--fuel N] [--stats] [-i | -e SOURCE | --check FILE | --dump-bytecode FILE | --fingerprint FILE | FILE | -] [-- ARG...]\n qcoffee --interactive\n qcoffee --version" @@ -116,17 +118,19 @@ fn main() -> ExitCode { }, "--stats" => stats = true, "-e" => match args.next() { - Some(s) => source = Some(s), + Some(s) if source.is_none() => source = Some(s), + Some(_) => { + eprintln!("only one source input is allowed"); + return ExitCode::from(2); + } None => { eprintln!("-e requires source text"); return ExitCode::from(2); } }, "--dump-bytecode" => { - if check || fingerprint || stats { - eprintln!( - "--check, --dump-bytecode, --fingerprint, and --stats are execution-mode alternatives" - ); + if source.is_some() || dump || check || fingerprint || stats { + eprintln!("{EXECUTION_MODE_ALTERNATIVES}"); return ExitCode::from(2); } dump = true; @@ -150,10 +154,8 @@ fn main() -> ExitCode { } } "--check" => { - if dump || fingerprint || stats { - eprintln!( - "--check, --dump-bytecode, --fingerprint, and --stats are execution-mode alternatives" - ); + if source.is_some() || dump || check || fingerprint || stats { + eprintln!("{EXECUTION_MODE_ALTERNATIVES}"); return ExitCode::from(2); } check = true; @@ -172,10 +174,8 @@ fn main() -> ExitCode { } } "--fingerprint" => { - if check || dump || stats { - eprintln!( - "--check, --dump-bytecode, --fingerprint, and --stats are execution-mode alternatives" - ); + if source.is_some() || dump || check || fingerprint || stats { + eprintln!("{EXECUTION_MODE_ALTERNATIVES}"); return ExitCode::from(2); } fingerprint = true; @@ -232,9 +232,7 @@ fn main() -> ExitCode { return ExitCode::from(2); }; if stats && (dump || check || fingerprint) { - eprintln!( - "--check, --dump-bytecode, --fingerprint, and --stats are execution-mode alternatives" - ); + eprintln!("{EXECUTION_MODE_ALTERNATIVES}"); return ExitCode::from(2); } let engine = Engine::new(); diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index e788cb3..718d574 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -139,6 +139,27 @@ fn qtest_reports_success_and_failure() { let _ = fs::remove_file(temp); } +#[cfg(unix)] +#[test] +fn qtest_ignores_recursive_directory_symlinks() { + use std::os::unix::fs::symlink; + + let temp = std::env::temp_dir().join(format!("qcoffee-qtest-cycle-{}", std::process::id())); + fs::create_dir_all(&temp).unwrap(); + fs::write(temp.join("pass.qc"), "true\n").unwrap(); + symlink(&temp, temp.join("loop")).unwrap(); + let output = Command::new(bin("qtest")) + .args(["--tap", temp.to_str().unwrap()]) + .output() + .unwrap(); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!("TAP version 13\nok 1 - {}/pass.qc\n1..1\n", temp.display()) + ); + let _ = fs::remove_dir_all(temp); +} + #[test] fn every_cli_reports_the_same_package_version() { for name in ["qcoffee", "qtest", "qdocco", "qbench"] { @@ -189,9 +210,9 @@ fn qtest_tap_output_is_deterministic_and_describes_failures() { assert_eq!( String::from_utf8_lossy(&output.stdout), format!( - "TAP version 13\nnot ok 1 - {}\n# final value was 1, expected true\nok 2 - {}\n1..2\n", - temp.join("a-fail.qc").display(), - temp.join("b-pass.qc").display() + "TAP version 13\nnot ok 1 - {}/a-fail.qc\n# final value was 1, expected true\nok 2 - {}/b-pass.qc\n1..2\n", + temp.display(), + temp.display() ) ); let reversed = Command::new(bin("qtest")) @@ -207,36 +228,9 @@ fn qtest_tap_output_is_deterministic_and_describes_failures() { assert_eq!( String::from_utf8_lossy(&reversed.stdout), format!( - "TAP version 13\nnot ok 1 - {}\n# final value was 1, expected true\nok 2 - {}\n1..2\n", - temp.join("a-fail.qc").display(), - temp.join("b-pass.qc").display() - ) - ); - let reversed_json = Command::new(bin("qtest")) - .args([ - "--json", - temp.join("b-pass.qc").to_str().unwrap(), - temp.join("a-fail.qc").to_str().unwrap(), - temp.join("a-fail.qc").to_str().unwrap(), - ]) - .output() - .unwrap(); - assert!(!reversed_json.status.success()); - let failed_path = temp - .join("a-fail.qc") - .display() - .to_string() - .replace('\\', "\\\\"); - let passed_path = temp - .join("b-pass.qc") - .display() - .to_string() - .replace('\\', "\\\\"); - assert_eq!( - String::from_utf8_lossy(&reversed_json.stdout), - format!( - "{{\"ok\":false,\"file\":\"{}\",\"error\":\"final value was 1, expected true\"}}\n{{\"ok\":true,\"file\":\"{}\"}}\n", - failed_path, passed_path + "TAP version 13\nnot ok 1 - {}/a-fail.qc\n# final value was 1, expected true\nok 2 - {}/b-pass.qc\n1..2\n", + temp.display(), + temp.display() ) ); let conflict = Command::new(bin("qtest")) @@ -331,6 +325,27 @@ fn qcoffee_evaluation_fuel_and_disassembly_match_the_cli_contract() { let temp = std::env::temp_dir().join(format!("qcoffee-dump-{}.qc", std::process::id())); fs::write(&temp, "1 + 2\n").unwrap(); + let conflicting_source = Command::new(bin("qcoffee")) + .args(["-e", "1", "--check", temp.to_str().unwrap()]) + .output() + .unwrap(); + assert_eq!(conflicting_source.status.code(), Some(2)); + assert!( + String::from_utf8_lossy(&conflicting_source.stderr).contains("execution-mode alternatives") + ); + assert!(String::from_utf8_lossy(&conflicting_source.stderr).contains("-e SOURCE, FILE, and -")); + + let repeated_dump = Command::new(bin("qcoffee")) + .args([ + "--dump-bytecode", + temp.to_str().unwrap(), + "--dump-bytecode", + temp.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!(repeated_dump.status.code(), Some(2)); + let dump = Command::new(bin("qcoffee")) .args(["--dump-bytecode", temp.to_str().unwrap()]) .output()