use super::*;
use crate::typescript::frameworks::{DEFAULT_REGISTRATIONS, REGISTRATIONS};
const LONG_LINE_BYTES: usize = 4_000;
use std::fs;
use std::io::Write;
fn authored_source() -> Vec<u8> {
let mut source = b"import { Given } from '@cucumber/cucumber';\n\n".to_vec();
for index in 0..200 {
source.extend_from_slice(
format!("Given('step number {index}', async function () {{\n await work();\n}});\n\n")
.as_bytes(),
);
}
source
}
fn one_line_minified() -> Vec<u8> {
format!("!function(n){{{}}}(0);", "var a=1,b=2,c=3;".repeat(500)).into_bytes()
}
fn wrapped_minified() -> Vec<u8> {
let mut bytes = Vec::new();
for _ in 0..40 {
bytes.extend_from_slice("var a=1,b=2,c=3;".repeat(32).as_bytes());
bytes.push(b'\n');
}
bytes
}
#[test]
fn classifies_every_content_kind_discovery_can_deliver() {
let mut blank_padded = wrapped_minified();
blank_padded.extend_from_slice(&vec![b'\n'; 400]);
let mut crlf_minified = Vec::new();
for line in wrapped_minified().split(|byte| *byte == b'\n') {
crlf_minified.extend_from_slice(line);
crlf_minified.extend_from_slice(b"\r\n");
}
let mut gzip = vec![0x1f, 0x8b, 0x08];
gzip.extend_from_slice(b"compressed payload that is otherwise textual");
let mut long_literal = authored_source();
long_literal.extend_from_slice(format!("const BLOB = '{}';\n", "x".repeat(5_000)).as_bytes());
let mut multibyte = Vec::new();
for _ in 0..50 {
multibyte.extend_from_slice("// 日本語のコメントがここに書かれています\n".as_bytes());
}
for (label, bytes, expected) in [
("authored source", authored_source(), None),
("empty file", Vec::new(), None),
("long literal in authored source", long_literal, None),
("multibyte authored source", multibyte, None),
(
"one-line minified",
one_line_minified(),
Some(ExcludedSource::Minified),
),
(
"wrapped minified",
wrapped_minified(),
Some(ExcludedSource::Minified),
),
(
"minified padded with blank lines",
blank_padded,
Some(ExcludedSource::Minified),
),
(
"minified with CRLF endings",
crlf_minified,
Some(ExcludedSource::Minified),
),
("gzip container", gzip, Some(ExcludedSource::Compressed)),
(
"NUL bytes",
b"var a = 1;\0\0\0binary\0".to_vec(),
Some(ExcludedSource::Binary),
),
(
"invalid UTF-8 without NUL",
b"const a = '\xff\xfe';\n".to_vec(),
None,
),
] {
assert_eq!(classify(&bytes, &[]), expected, "{label}");
}
}
#[test]
fn a_line_wider_than_the_whole_prefix_is_minified_whatever_precedes_it() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("vendor.min.js");
let mut file = fs::File::create(&path).unwrap();
file.write_all(b"/*! library v1.2.3 | (c) authors | license MIT */\n")
.unwrap();
file.write_all(&one_line_minified()).unwrap();
file.write_all(&vec![b'z'; SOURCE_CLASSIFICATION_PREFIX_BYTES])
.unwrap();
drop(file);
assert_eq!(inspect(&path, &[]), Some(ExcludedSource::Minified));
}
#[test]
fn authored_sources_longer_than_the_prefix_are_still_analyzed() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("steps.ts");
let mut bytes = authored_source();
while bytes.len() <= SOURCE_CLASSIFICATION_PREFIX_BYTES {
bytes.extend_from_slice(&authored_source());
}
bytes.extend_from_slice(b"Given('unterminated last line', () => work());");
fs::write(&path, &bytes).unwrap();
assert_eq!(inspect(&path, &[]), None);
}
#[test]
fn an_unreadable_path_is_left_for_extraction_to_report() {
assert_eq!(inspect(Path::new("/nonexistent/vendor.js"), &[]), None);
}
#[test]
fn the_guard_vocabulary_covers_every_registration_the_analyzer_understands() {
let guarded = QUALIFIABLE_REGISTRATIONS
.iter()
.chain(BARE_ONLY_REGISTRATIONS.iter());
let guarded = guarded.collect::<Vec<_>>();
for name in REGISTRATIONS.iter().chain(DEFAULT_REGISTRATIONS.iter()) {
assert!(
guarded.contains(&name),
"registration `{name}` is not guarded"
);
}
}
#[test]
fn authored_code_that_is_one_enormous_line_survives_on_its_registrations() {
let authored = format!(
"Given('{}', () => work());",
str::repeat("a", LONG_LINE_BYTES)
);
assert_eq!(classify(authored.as_bytes(), &[]), None);
let generated = format!("!function(){{{}}}();", str::repeat("a", LONG_LINE_BYTES));
assert_eq!(
classify(generated.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn a_configured_registration_name_also_guards_a_file() {
let source = format!(
"myStep('{}', () => work());",
str::repeat("a", LONG_LINE_BYTES)
);
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
assert_eq!(classify(source.as_bytes(), &["myStep".to_owned()]), None);
}
#[test]
fn a_registration_name_inside_a_longer_identifier_does_not_guard_a_bundle() {
for callee in ["promisedPatchThen", "splitWhen", "myGiven", "define$Step"] {
let bundle = format!(
"!function(){{{}({})}}();",
callee,
"a,".repeat(LONG_LINE_BYTES / 2)
);
assert_eq!(
classify(bundle.as_bytes(), &[]),
Some(ExcludedSource::Minified),
"{callee}"
);
}
}
#[test]
fn a_namespace_qualified_registration_guards_a_file_but_a_promise_chain_does_not() {
let filler = "var a=1,b=2,c=3;".repeat(500);
let qualified = format!("{filler}cucumber.Given('a step', () => work());{filler}");
assert_eq!(classify(qualified.as_bytes(), &[]), None);
let chained = format!("{filler}p.then(function(){{return 1}});{filler}");
assert_eq!(
classify(chained.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn a_bare_lowercase_alias_guards_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
let aliased = format!("{filler}then('a step', () => work());{filler}");
assert_eq!(classify(aliased.as_bytes(), &[]), None);
}
#[test]
fn a_renaming_import_guards_a_file_through_its_module_path() {
let filler = "var a=1,b=2,c=3;".repeat(500);
let renamed = format!(
"import {{ Given as G }} from '@cucumber/cucumber';{filler}G('a step', () => work());{filler}"
);
assert_eq!(classify(renamed.as_bytes(), &[]), None);
let anonymous = format!("{filler}G('a step', () => work());{filler}");
assert_eq!(
classify(anonymous.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn a_project_facade_import_guards_a_file_whatever_the_callee_form() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for importer in [
"import { G } from './facade.js';",
"import { G } from '../steps/facade';",
"export { G } from \"./facade\";",
"const { G } = require('./facade');",
"const { G } = await import('./facade');",
"import './facade';",
"import { G } from /* facade */ './facade';",
"import { G } from // facade\n './facade';",
"export { G } from './facade';",
"import { Given as Action } from '~/facade';",
"import { Given as Action } from '@/steps/facade';",
"import { Given as Action } from '#steps/facade';",
] {
let source = format!("{importer}{filler}G<string>('a step', () => work());{filler}");
assert_eq!(classify(source.as_bytes(), &[]), None, "{importer}");
}
}
#[test]
fn a_project_specifier_outside_import_position_does_not_guard_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for bundled in [
"var p = \"./assets/icon.png\";",
"t.resolve(\"./chunk\");",
"var base = '../';",
"import x from 'lodash';",
"var m = \"No conversion from \" + u;",
] {
let source = format!("{filler}{bundled}{filler}");
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified),
"{bundled}"
);
}
}
#[test]
fn every_registered_framework_module_guards_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for module in crate::typescript::frameworks::registration_modules() {
let source = format!("import {{ X }} from '{module}';{filler}X('s', () => work());");
assert_eq!(classify(source.as_bytes(), &[]), None, "{module}");
}
}
#[test]
fn comment_trivia_between_the_name_and_the_call_still_guards_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for spaced in [
"Given/* matcher below */('a step', () => work());",
"Given // matcher below\n('a step', () => work());",
"Given\n ('a step', () => work());",
] {
let source = format!("{filler}{spaced}{filler}");
assert_eq!(classify(source.as_bytes(), &[]), None, "{spaced}");
}
let mentioned = format!("{filler}var Given = 1, When = 2, Step = 3;{filler}");
assert_eq!(
classify(mentioned.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn every_transparent_callee_wrapper_still_guards_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for wrapped in [
"(Given)('a step', () => work());",
"((Given))('a step', () => work());",
"(Given) /* wrapped */ ('a step', () => work());",
"Given!('a step', () => work());",
"Given<string>('a step', () => work());",
"Given<Map<string, number>>('a step', () => work());",
"(Given as typeof Given)('a step', () => work());",
"(Given satisfies Registration)('a step', () => work());",
"(Given!)('a step', () => work());",
"(Given as { (pattern: string, fn: () => void): void })('a step', () => work());",
"(Given as (pattern: string) => void)('a step', () => work());",
"(Given as Record<string, () => void>)('a step', () => work());",
"(Given as Map<() => void, string>)('a step', () => work());",
"Given<() => void>('a step', () => work());",
"Given<Array<() => void>>('a step', () => work());",
"Given<{ value: string }>('a step', () => work());",
"Given<{ value: { nested: string } }>('a step', () => work());",
"Given<{ value: string; other: number }>('a step', () => work());",
"Given<Record<string, { value: string }>>('a step', () => work());",
] {
let source = format!("{filler}{wrapped}{filler}");
assert_eq!(classify(source.as_bytes(), &[]), None, "{wrapped}");
}
let compared = format!("{filler}if(Given<b){{c()}};var d=e>(f);{filler}");
assert_eq!(
classify(compared.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
let argument = format!("{filler}register(Given);{filler}");
assert_eq!(
classify(argument.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn a_bzip2_header_needs_its_whole_signature() {
assert_eq!(
classify(b"BZh91AY&SY\x00payload", &[]),
Some(ExcludedSource::Compressed)
);
for authored in [
"BZh = 1;\nGiven('a step', () => work());\n",
"BZh0 = 1;\nGiven('a step', () => work());\n",
"BZh9 = 1;\nGiven('a step', () => work());\n",
"BZh91AYNOT = 1;\nGiven('a step', () => work());\n",
"BZh01AY&SY = 1;\nGiven('a step', () => work());\n",
] {
assert_eq!(classify(authored.as_bytes(), &[]), None, "{authored}");
}
}
#[test]
fn ecmascript_whitespace_beyond_ascii_still_guards_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for space in [
'\u{00a0}', '\u{2003}', '\u{2028}', '\u{2029}', '\u{3000}', '\u{feff}',
] {
let source = format!("{filler}Given{space}('a step', () => work());{filler}");
assert_eq!(
classify(source.as_bytes(), &[]),
None,
"U+{:04X}",
space as u32
);
}
let translated = format!("{filler}var 日本語={{}};var t=\"ステップ\";{filler}");
assert_eq!(
classify(translated.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn each_exclusion_reports_its_own_word() {
for (bytes, expected, word) in [
(
b"BZh91AY&SY\x00payload".to_vec(),
ExcludedSource::Compressed,
"compressed",
),
(b"var a = 1;\0".to_vec(), ExcludedSource::Binary, "binary"),
(
"var a=1,b=2,c=3;".repeat(500).into_bytes(),
ExcludedSource::Minified,
"minified",
),
] {
assert_eq!(classify(&bytes, &[]), Some(expected));
assert_eq!(expected.as_str(), word);
}
}
#[test]
fn an_import_keyword_reached_through_a_member_access_is_not_an_import() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for member in [
"t.from('./chunk')",
"t.require('./chunk')",
"t.import('./chunk')",
] {
let source = format!("{filler}{member};{filler}");
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified),
"{member}"
);
}
}
#[test]
fn an_unterminated_comment_after_an_import_keyword_is_not_an_import() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for broken in ["import(/* never closed", "from /* never closed"] {
let source = format!("{filler}{broken}{filler}");
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified),
"{broken}"
);
}
}
#[test]
fn a_type_operator_that_runs_to_the_end_of_the_prefix_is_not_a_call() {
let filler = "var a=1,b=2,c=3;".repeat(500);
let source = format!(
"{filler}Given as SomeVeryLongTypeName{}",
"Alias".repeat(200)
);
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn a_value_block_inside_a_comparison_chain_does_not_guard_a_file() {
let filler = "var a=1,b=2,c=3;".repeat(500);
for chain in [
"var r = Given < class { field; } > ('x');",
"var r = Given < class Named { field; } > ('x');",
"var r = Given < function () { work(); } > ('x');",
"var r = Given < class { first; second; } > ('x');",
] {
let source = format!("{filler}{chain}{filler}");
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified),
"{chain}"
);
}
}
#[test]
fn an_unterminated_comment_does_not_run_past_the_prefix() {
let filler = "var a=1,b=2,c=3;".repeat(500);
let source = format!("{filler}Given/* never closed{filler}");
assert_eq!(
classify(source.as_bytes(), &[]),
Some(ExcludedSource::Minified)
);
}
#[test]
fn repeated_unbalanced_type_arguments_do_not_scale_quadratically() {
let prefix = "Given<"
.repeat(SOURCE_CLASSIFICATION_PREFIX_BYTES / 6)
.into_bytes();
let started = std::time::Instant::now();
let verdict = classify(&prefix, &[]);
let elapsed = started.elapsed();
assert_eq!(verdict, Some(ExcludedSource::Minified));
assert!(
elapsed < std::time::Duration::from_millis(500),
"classification took {elapsed:?} on {} bytes of unbalanced type arguments",
prefix.len()
);
}
#[test]
fn a_type_argument_list_longer_than_any_cap_still_guards_a_file() {
let union = (0..400)
.map(|index| format!("Type{index}"))
.collect::<Vec<_>>()
.join(" | ");
let source = format!("Given<{union}>('a step', () => work());");
assert!(
source.len() > 2_048,
"the type list must exceed any plausible cap"
);
assert_eq!(classify(source.as_bytes(), &[]), None);
}
#[test]
fn a_large_registration_configuration_does_not_multiply_classification_cost() {
let prefix = "var a=1,b=2,c=3;"
.repeat(SOURCE_CLASSIFICATION_PREFIX_BYTES / 16)
.into_bytes();
let names = (0..20_000)
.map(|index| format!("absentRegistration{index}"))
.collect::<Vec<_>>();
let started = std::time::Instant::now();
let verdict = classify(&prefix, &names);
let elapsed = started.elapsed();
assert_eq!(verdict, Some(ExcludedSource::Minified));
assert!(
elapsed < std::time::Duration::from_secs(2),
"classification took {elapsed:?} with {} configured names",
names.len()
);
}
#[test]
fn binary_and_compressed_content_is_excluded_even_when_it_mentions_a_registration() {
let mut binary = b"Given('step', () => work());".to_vec();
binary.push(0);
assert_eq!(classify(&binary, &[]), Some(ExcludedSource::Binary));
let mut compressed = vec![0x1f, 0x8b, 0x08];
compressed.extend_from_slice(b"Given('step', () => work());");
assert_eq!(classify(&compressed, &[]), Some(ExcludedSource::Compressed));
}