1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//! schema-cleanup-v2: close 4 LOW bugs sharing the surface
//! "schema/UX polish".
//!
//! - P2.BUG-6 — `tldr clones --format dot` silently emitted JSON (exit 0)
//! even though `secure --format dot`'s error message and the per-command
//! DOT validator advertised clones as DOT-supporting. The clones run
//! loop now wires the canonical `--format dot` route to the existing
//! `format_clones_dot` emitter.
//!
//! - P2.BUG-7 — Clones JSON `language` field always echoed `"auto"` (or
//! the user's `--lang` flag verbatim), making it impossible to tell what
//! the autodetector actually picked. The field is now resolved to the
//! dominant language string across discovered files.
//!
//! - P2.BUG-9 — `tldr vuln` findings carried no `function` field, blocking
//! clean piping into `tldr taint <file> <function>` and `tldr slice
//! <file> <function> <line>`. Findings now carry an `Option<String>`
//! `function` field populated from the `extract_file` AST extractor.
//!
//! - P2.BUG-10 — Empty-dir handling was inconsistent: `structure`/`calls`/
//! `vuln` returned exit 0 with empty results while `health` returned
//! exit 23, `deps` returned exit 11, and `churn` returned exit 1.
//! `calls` also silently defaulted `language: "python"` for empty dirs.
//! All commands now treat empty directories as a benign edge case with
//! exit 0 + empty results + a `warnings` field, and `calls` reports
//! `language: null` rather than silently defaulting to Python.
use assert_cmd::prelude::*;
use serde_json::Value;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn tldr_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("tldr"))
}
// =============================================================================
// Helpers
// =============================================================================
/// Build a small Python project with two near-duplicate functions so the
/// clones detector has something to find regardless of host environment.
/// Returns the TempDir so the caller can keep it alive for the test.
fn make_clones_project() -> TempDir {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let body = r#"
def alpha(x):
total = 0
for i in range(x):
total += i * i
return total
def beta(y):
total = 0
for i in range(y):
total += i * i
return total
def gamma(z):
total = 0
for i in range(z):
total += i * i
return total
"#;
fs::write(dir.join("a.py"), body).unwrap();
fs::write(dir.join("b.py"), body).unwrap();
temp
}
/// Build a Python project with an obvious vulnerable taint flow inside a
/// named function so the vuln finding can report a non-null `function`
/// field. Uses the canonical Flask `request.args.get` → `cursor.execute`
/// f-string pattern recognised by the canonical taint engine (mirrors
/// the PYTHON_VULN_SQLI fixture in `remaining_test.rs`).
fn make_vuln_project() -> TempDir {
let temp = TempDir::new().unwrap();
let body = r#"
from flask import Flask, request
import sqlite3
app = Flask(__name__)
@app.route('/search')
def search():
user_query = request.args.get('q')
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM products WHERE name LIKE '%{user_query}%'")
return cursor.fetchall()
"#;
fs::write(temp.path().join("vuln.py"), body).unwrap();
temp
}
// =============================================================================
// P2.BUG-6: `tldr clones --format dot` emits valid DOT
// =============================================================================
#[test]
fn clones_dot_output_valid() {
let project = make_clones_project();
let out = tldr_cmd()
.args(["clones", "-q", "--format", "dot"])
.arg(project.path())
.output()
.expect("clones --format dot");
assert!(
out.status.success(),
"clones --format dot must exit 0; got {:?}\nstderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.starts_with("digraph clones {"),
"clones --format dot must start with 'digraph clones {{'; got:\n{}",
stdout
);
// The fixture has 3 byte-identical functions across 2 files, so the
// clone detector must produce at least one pair → at least one DOT
// edge. (An empty `digraph clones {{}}` would also be a valid empty
// DOT document, but for THIS fixture we expect non-empty.)
let edge_count = stdout.matches(" -> ").count();
assert!(
edge_count >= 1,
"fixture has obvious clones; expected >=1 DOT edges, got {} in:\n{}",
edge_count,
stdout
);
}
// =============================================================================
// P2.BUG-7: clones `language` resolves to the actual analyzed language
// =============================================================================
#[test]
fn clones_language_resolved() {
let project = make_clones_project();
// No --lang: must autodetect to "python" (not "auto").
let out = tldr_cmd()
.args(["clones", "-q"])
.arg(project.path())
.output()
.expect("clones");
assert!(out.status.success());
let report: Value = serde_json::from_slice(&out.stdout).expect("clones JSON");
let lang = report["language"].as_str().expect("language field present");
assert_eq!(
lang, "python",
"autodetect on .py-only fixture must resolve to 'python', got {:?}",
lang
);
// Explicit --lang python: must still report "python" (not "auto").
let out = tldr_cmd()
.args(["clones", "-q", "--language", "python"])
.arg(project.path())
.output()
.expect("clones --language python");
assert!(out.status.success());
let report: Value = serde_json::from_slice(&out.stdout).expect("clones JSON");
let lang = report["language"].as_str().expect("language field present");
assert_eq!(lang, "python");
}
// =============================================================================
// P2.BUG-9: vuln findings carry an enclosing `function` field
// =============================================================================
#[test]
fn vuln_finding_has_function_field() {
let project = make_vuln_project();
let out = tldr_cmd()
.args(["vuln", "-q", "--lang", "python"])
.arg(project.path())
.output()
.expect("vuln");
assert!(
out.status.success(),
"vuln must exit 0; got {:?}\nstderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
let report: Value = serde_json::from_slice(&out.stdout).expect("vuln JSON");
let findings = report["findings"]
.as_array()
.expect("findings array present");
assert!(
!findings.is_empty(),
"fixture has an obvious SQL injection; expected >=1 finding, got 0:\n{}",
String::from_utf8_lossy(&out.stdout)
);
// Each finding either reports a non-empty function name OR is at
// module scope (function field omitted because the value is None
// and the field is `skip_serializing_if = "Option::is_none"`). For
// THIS fixture the source line lives inside `search`, so we expect
// the function to be set.
let f0 = &findings[0];
let func = f0
.get("function")
.and_then(|v| v.as_str())
.expect("function field set for an in-function finding");
assert_eq!(
func, "search",
"expected enclosing function 'search', got {:?}",
func
);
}
// =============================================================================
// P2.BUG-10: uniform empty-dir exit code (0) across structural commands
// =============================================================================
#[test]
fn empty_dir_uniform_exit_zero() {
let empty = TempDir::new().unwrap();
// The 6 commands listed in the bug repro. They cover the structural
// surface (`structure`), the call-graph surface (`calls`), the
// health/quality surface (`health`), the dependency surface
// (`deps`), the git-history surface (`churn`), and the security
// surface (`vuln`). Each must treat an empty directory as a
// benign edge case (exit 0).
for cmd in &["structure", "calls", "health", "deps", "churn", "vuln"] {
let out = tldr_cmd()
.args([cmd, "-q"])
.arg(empty.path())
.output()
.unwrap_or_else(|e| panic!("{}: failed to spawn: {}", cmd, e));
assert!(
out.status.success(),
"{} on empty dir must exit 0; got {:?}\nstderr: {}",
cmd,
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
}
}
// =============================================================================
// P2.BUG-10: `calls` does not silently default `language: "python"` on
// empty input
// =============================================================================
#[test]
fn calls_empty_dir_no_default_language() {
let empty = TempDir::new().unwrap();
let out = tldr_cmd()
.args(["calls", "-q"])
.arg(empty.path())
.output()
.expect("calls");
assert!(
out.status.success(),
"calls on empty dir must exit 0; got {:?}\nstderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
let report: Value = serde_json::from_slice(&out.stdout).expect("calls JSON");
// The `language` field MUST NOT be the literal string "python" — that
// was the silent fallback the bug reported. Acceptable values are
// JSON null, the literal string "unknown", or the field's outright
// omission. A non-null string that equals an actual language is
// ALSO not acceptable here (the dir is empty, no language was
// analysed).
let lang = &report["language"];
let ok = lang.is_null()
|| lang.as_str() == Some("unknown")
|| lang.as_str() == Some("auto");
assert!(
ok,
"calls on empty dir must report language: null|\"unknown\"|\"auto\", \
not silently default to a real language; got {:?}\nfull report:\n{}",
lang, report
);
}