Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deno Doctest Support #6124

Closed
wants to merge 57 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
68dfc4f
Merge pull request #1 from denoland/master
iykekings May 27, 2020
0c80a0b
first working draft
iykekings Jun 5, 2020
27a99ff
Merge pull request #2 from denoland/master
iykekings Jun 5, 2020
c506d94
Merge branch 'master' of https://github.com/iykekings/deno
iykekings Jun 5, 2020
2756a47
use Mainwoker::create
iykekings Jun 5, 2020
f811f16
format
iykekings Jun 6, 2020
c2d09b7
Merge pull request #3 from iykekings/lint
iykekings Jun 6, 2020
041c368
General refactoring (#4)
iykekings Jun 6, 2020
6b00045
make clippy happy
iykekings Jun 6, 2020
dfaae34
use deno test --docs (#5)
iykekings Jun 6, 2020
1de2418
clippy :(
iykekings Jun 6, 2020
d7065c8
refactor: remove Doctest from DenoSubcommand
iykekings Jun 6, 2020
2d8d5eb
Add tests (#6)
iykekings Jun 7, 2020
dcca3db
x
iykekings Jun 7, 2020
d6d5157
xx
iykekings Jun 7, 2020
6cdd2fd
another empty commit
iykekings Jun 7, 2020
3516cd8
Merge branch 'master' of https://github.com/denoland/deno into denola…
iykekings Aug 25, 2020
e922713
fix conflict
iykekings Aug 25, 2020
49a6e6c
Merge branch 'denoland-master'
iykekings Aug 25, 2020
91d7962
Merge branch 'master' of https://github.com/denoland/deno into denola…
iykekings Sep 10, 2020
bca1ea0
Merge branch 'denoland-master'
iykekings Sep 10, 2020
be1aaeb
parse jsdoc with jsdoc crate
iykekings Sep 20, 2020
64cc8ae
Merge branch 'master' of https://github.com/denoland/deno into upstre…
iykekings Sep 21, 2020
c3068f2
Merge branch 'upstream-master'
iykekings Sep 21, 2020
7bcecc7
Merge pull request #9 from denoland/master
iykekings Sep 21, 2020
b5220fb
fix tests
iykekings Sep 24, 2020
5c77b29
completed rewrite
iykekings Sep 24, 2020
85ebc0a
issues with submodule
iykekings Sep 24, 2020
53c8f6d
Merge branch 'master' into iykekings_master
bartlomieju Sep 25, 2020
b934b58
fix2
bartlomieju Sep 25, 2020
178cf12
update
iykekings Sep 25, 2020
c81cd80
rewrite completed
iykekings Oct 8, 2020
48d8f13
Merge branch 'master' into master
iykekings Oct 8, 2020
062ad63
sync with master
bartlomieju Oct 8, 2020
c68e726
sync with master2
bartlomieju Oct 8, 2020
4d0f419
update fork
iykekings Oct 9, 2020
8ec0109
harmonize changes
iykekings Oct 9, 2020
4f6ab34
don't die on invalid jsdoc
iykekings Oct 9, 2020
ea90be5
refactor
iykekings Oct 9, 2020
bf4f318
use example span for line number
iykekings Oct 9, 2020
c33e426
remove cli/js
iykekings Oct 9, 2020
3414bec
require unstable flag
iykekings Oct 9, 2020
158128e
update third_party
bartlomieju Oct 10, 2020
ad5ba48
updates
iykekings Oct 10, 2020
fe03e92
docs(std/bytes): add missing docs to README (#7885)
kt3k Oct 8, 2020
e2a65c1
Fix typos (#7882)
crowlKats Oct 8, 2020
f61678d
fix(op_crates/fetch): Stringify and parse Request URLs (#7838)
nayeemrmn Oct 9, 2020
56c18a3
build: invalidate GHA cache (#7894)
bartlomieju Oct 9, 2020
0e04e09
ci: fix rusty_v8 binary download unavailable (#7898)
piscisaureus Oct 9, 2020
0fc4c46
ci: add workaround for MacOS + Cargo + Github Actions cache bug (#7898)
piscisaureus Oct 9, 2020
1dc00ff
fix Releases.md (#7883)
bartlomieju Oct 9, 2020
713a3ba
refactor: Worker is not a Future (#7895)
bartlomieju Oct 9, 2020
6ad8de1
Implement Serialize for ModuleSpecifier (#7900)
ry Oct 9, 2020
80c285e
docs: add Deno internals talk from Paris Deno (#7889)
trivikr Oct 10, 2020
57496b8
fix(op_crate/web): add padding on URLSearchParam (#7905)
lala7573 Oct 10, 2020
19846bc
Fix 100% CPU idling problem by reverting #7672 (#7911)
ry Oct 10, 2020
109f2ba
v1.4.6
bartlomieju Oct 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions cli/doctest_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
use regex::Regex;
use std::path::{Path, PathBuf};

use crate::fs as deno_fs;
use crate::installer::is_remote_url;
use crate::test_runner::is_supported;

pub struct DocTest {
// This removes repetition of imports in a file
imports: std::collections::HashSet<String>,
// This contains codes in an @example section with their imports removed
bodies: Vec<DocTestBody>,
}

struct DocTestBody {
caption: String,
line_number: usize,
path: PathBuf,
value: String,
ignore: bool,
is_async: bool,
}

pub fn prepare_doctest(
include: Vec<String>,
// root_path: &PathBuf,
) -> Vec<DocTest> {
let include_paths: Vec<_> =
include.into_iter().filter(|n| !is_remote_url(n)).collect();
iykekings marked this conversation as resolved.
Show resolved Hide resolved

let mut prepared = vec![];

for path in include_paths {
let p = deno_fs::normalize_path(&Path::new(&path));
if p.is_dir() {
let test_files = crate::fs::files_in_subtree(p, |p| {
iykekings marked this conversation as resolved.
Show resolved Hide resolved
let supported_files = ["ts", "tsx", "js", "jsx"];
match p.extension().and_then(std::ffi::OsStr::to_str) {
Some(x) => supported_files.contains(&x) && !is_supported(p),
iykekings marked this conversation as resolved.
Show resolved Hide resolved
_ => false,
}
});
prepared.extend(test_files);
} else {
prepared.push(p);
}
}

prepared
.iter()
.filter_map(|dir| {
// TODO(iykekings) use deno error instead
let content = std::fs::read_to_string(&dir).expect(
iykekings marked this conversation as resolved.
Show resolved Hide resolved
format!("File doesn't exist {}", dir.to_str().unwrap_or("")).as_str(),
iykekings marked this conversation as resolved.
Show resolved Hide resolved
);
extract_jsdoc_examples(content, dir.to_owned())
})
.collect::<Vec<_>>()
}

fn extract_jsdoc_examples(input: String, p: PathBuf) -> Option<DocTest> {
iykekings marked this conversation as resolved.
Show resolved Hide resolved
lazy_static! {
static ref JS_DOC_PATTERN: Regex =
Regex::new(r"/\*\*\s*\n([^\*]|\*[^/])*\*/").unwrap();
// IMPORT_PATTERN doesn't match dynamic imports
static ref IMPORT_PATTERN: Regex =
Regex::new(r"import[^(].*\n").unwrap();
static ref EXAMPLE_PATTERN: Regex = Regex::new(r"@example\s*(?:<\w+>.*</\w+>)*\n(?:\s*\*\s*\n*)*```").unwrap();
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
static ref TICKS_OR_IMPORT_PATTERN: Regex = Regex::new(r"(?:import[^(].*)|(?:```\w*)").unwrap();
static ref CAPTION_PATTERN: Regex = Regex::new(r"<caption>([\s\w\W]+)</caption>").unwrap();
static ref TEST_TAG_PATTERN: Regex = Regex::new(r"@example\s*(?:<\w+>.*</\w+>)*\n(?:\s*\*\s*\n*)*```(\w+)").unwrap();
static ref AWAIT_PATTERN: Regex = Regex::new(r"\Wawait\s").unwrap();
}

let mut import_set = std::collections::HashSet::new();
iykekings marked this conversation as resolved.
Show resolved Hide resolved

let test_bodies = JS_DOC_PATTERN
.captures_iter(&input)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about the use of regex here. I'm fine for using regex to extract example code from jsdoc comments, but we have a complete TypeScript parser (SWC) built-in. We should be using this to extract the JSDocs. There should be examples of this in cli/doc/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there is no way to recursively loop through DocNodes returned by the parser to get all the JSDocs but @bartlomieju promises to include the feature by next week. Should I go on with this or hold off till that feature is ready?

.filter_map(|caps| caps.get(0).map(|c| (c.start(), c.as_str())))
.flat_map(|(offset, section)| {
EXAMPLE_PATTERN.find_iter(section).filter_map(move |cap| {
section[cap.end()..].find("```").map(|i| {
(
offset + cap.end(),
section[cap.start()..i + cap.end()].to_string(),
)
})
})
})
.filter_map(|(offset, example_section)| {
let test_tag = TEST_TAG_PATTERN
.captures(&example_section)
.and_then(|m| m.get(1).map(|c| c.as_str()));

if test_tag == Some("text") {
return None;
}

IMPORT_PATTERN
.captures_iter(&example_section)
.filter_map(|caps| caps.get(0).map(|m| m.as_str()))
.for_each(|import| {
import_set.insert(import.to_string());
});

let caption = CAPTION_PATTERN
.captures(&example_section)
.and_then(|cap| cap.get(1).map(|m| m.as_str()))
.unwrap_or("");

let line_number = &input[0..offset].lines().count();

let body = TICKS_OR_IMPORT_PATTERN
.replace_all(&example_section, "\n")
.lines()
.skip(1)
.filter_map(|line| {
let res = match line.trim_start().starts_with("*") {
true => line.replacen("*", "", 1).trim_start().to_string(),
false => line.trim_start().to_string(),
};
match res.len() {
0 => None,
_ => Some(format!(" {}", res)),
}
})
.collect::<Vec<_>>()
.join("\n");
let is_async = match AWAIT_PATTERN.find(&example_section) {
Some(_) => true,
_ => false,
};
Some(DocTestBody {
caption: caption.to_owned(),
line_number: line_number.clone(),
path: p.clone(),
value: body,
ignore: test_tag == Some("ignore"),
is_async,
})
})
.collect::<Vec<_>>();

match test_bodies.len() {
0 => None,
_ => Some(DocTest {
imports: import_set,
bodies: test_bodies,
}),
}
}

pub fn render_doctest_file(
doctests: Vec<DocTest>,
fail_fast: bool,
quiet: bool,
filter: Option<String>,
) -> String {
let mut test_file = "".to_string();

// TODO(iykekings) - discuss with team if this is fine
let default_import = "import {
assert,
assertArrayContains,
assertEquals,
assertMatch,
assertNotEquals,
assertStrContains,
assertStrictEq,
assertThrows,
assertThrowsAsync,
equal,
unimplemented,
unreachable,
} from \"https://deno.land/std/testing/asserts.ts\";\n";
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved

test_file.push_str(default_import);

let all_imports: String = doctests
.iter()
.map(|doctest| doctest.imports.clone())
.flatten()
.collect();

test_file.push_str(&all_imports);
test_file.push_str("\n");

let all_test_section = doctests
.into_iter()
.map(|doctest| doctest.bodies.into_iter())
.flatten()
.map(|test| {
let async_str = if test.is_async {"async "} else {""};
format!(
"Deno.test({{\n\tname: \"{} -> {} (line {})\",\n\tignore: {},\n\t{}fn() {{\n{}\n}}\n}});\n",
test.path.display(),
test.caption,
test.line_number,
test.ignore,
async_str,
test.value
)
})
.collect::<Vec<_>>()
.join("\n");

test_file.push_str(&all_test_section);

let options = if let Some(filter) = filter {
json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet, "isDoctest": true, "filter": filter })
} else {
json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet, "isDoctest": true })
};

let run_tests_cmd = format!(
"\n// @ts-ignore\nDeno[Deno.internal].runTests({});\n",
options
);

test_file.push_str(&run_tests_cmd);

test_file
}
77 changes: 77 additions & 0 deletions cli/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ pub enum DenoSubcommand {
source_file: Option<String>,
filter: Option<String>,
},
Doctest {
fail_fast: bool,
quiet: bool,
allow_none: bool,
include: Option<Vec<String>>,
filter: Option<String>,
},
Eval {
code: String,
as_typescript: bool,
Expand Down Expand Up @@ -259,6 +266,8 @@ pub fn flags_from_vec_safe(args: Vec<String>) -> clap::Result<Flags> {
upgrade_parse(&mut flags, m);
} else if let Some(m) = matches.subcommand_matches("doc") {
doc_parse(&mut flags, m);
} else if let Some(m) = matches.subcommand_matches("doctest") {
doctest_parse(&mut flags, m);
} else {
repl_parse(&mut flags, &matches);
}
Expand Down Expand Up @@ -313,6 +322,7 @@ If the flag is set, restrict these messages to errors.",
.subcommand(types_subcommand())
.subcommand(upgrade_subcommand())
.subcommand(doc_subcommand())
.subcommand(doctest_subcommand())
.long_about(DENO_HELP)
.after_help(ENV_VARIABLES_HELP)
}
Expand Down Expand Up @@ -576,6 +586,33 @@ fn doc_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
};
}

fn doctest_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
run_test_args_parse(flags, matches);

let failfast = matches.is_present("failfast");
let allow_none = matches.is_present("allow_none");
let quiet = matches.is_present("quiet");
let filter = matches.value_of("filter").map(String::from);
let include = if matches.is_present("files") {
let files: Vec<String> = matches
.values_of("files")
.unwrap()
.map(String::from)
.collect();
Some(files)
} else {
None
};

flags.subcommand = DenoSubcommand::Doctest {
include,
fail_fast: failfast,
quiet,
filter,
allow_none,
};
}

fn types_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("types")
.arg(unstable_arg())
Expand Down Expand Up @@ -878,6 +915,46 @@ Show documentation for runtime built-ins:
)
}

fn doctest_subcommand<'a, 'b>() -> App<'a, 'b> {
run_test_args(SubCommand::with_name("doctest"))
.arg(
Arg::with_name("failfast")
.long("failfast")
.help("Stop on first error")
.takes_value(false),
)
.arg(
Arg::with_name("allow_none")
.long("allow-none")
.help("Don't return error code if no files with doctests are found")
.takes_value(false),
)
.arg(
Arg::with_name("filter")
.long("filter")
.takes_value(true)
.help("A pattern to filter the doctests to run by"),
)
.arg(
Arg::with_name("files")
.help("List of file names to run")
.takes_value(true)
.multiple(true),
)
.about("Run Doctests")
.long_about(
"Run doctests using Deno's built-in doctest runner.

Evaluate the given modules, run all doctests declared in JSDoc @example block and
report results to standard output:
deno doctest src/fetch.ts src/signal.ts

Directory arguments are expanded to all contained files matching the glob
{*_,*.,}.{js,ts,jsx,tsx} except test files:
deno doctest src/",
)
}

fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
app
.arg(
Expand Down
Loading