// SPDX-License-Identifier: Apache-2.0 OR MIT
use std::{error::Error as _, path::Path};
use fs_err as fs;
use parse_changelog::*;
use test_helper::git::assert_diff;
fn fixtures_dir() -> &'static Path {
Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"))
}
#[test]
fn success() {
fn trim(s: &str) -> &str {
let mut cnt = 0;
while s[cnt..].starts_with(' ') {
cnt += 1;
}
// Indents less than 4 are ignored.
if cnt < 4 { s[cnt..].trim_end() } else { s.trim_end() }
}
let changelogs = [
// Atx-style 1
"
# Changelog
## unreleased
## 0.2.0
0.2.0.
## 0.1.0
0.1.0.
",
// Atx-style 2
"
# Changelog
# unreleased
# 0.2.0
0.2.0.
# 0.1.0
0.1.0.
",
// Atx-style & 3 indents
"
# Changelog
# unreleased
# 0.2.0
0.2.0.
# 0.1.0
0.1.0.
",
// Setext-style 1
"
Changelog
==
unreleased
--
0.2.0
--
0.2.0.
0.1.0
--
0.1.0.
",
// Setext-style 2
"
Changelog
==
unreleased
==
0.2.0
==
0.2.0.
0.1.0
==
0.1.0.
",
// Setext-style & 3 indents
"
Changelog
==
unreleased
==
0.2.0
==
0.2.0.
0.1.0
==
0.1.0.
",
];
for changelog in &changelogs {
let changelog = parse(changelog).unwrap();
assert_eq!(changelog[0].title, "0.2.0");
assert_eq!(trim(changelog[0].notes), "0.2.0.");
assert_eq!(changelog["0.2.0"].title, "0.2.0");
assert_eq!(trim(changelog["0.2.0"].notes), "0.2.0.");
assert_eq!(changelog["0.1.0"].title, "0.1.0");
assert_eq!(trim(changelog["0.1.0"].notes), "0.1.0.");
}
}
#[test]
fn failure() {
let changelogs = [
// Atx-style & 4 indents
" ## 0.1.0\n",
// Setext-style & 4 indents
" 0.1.0\n ==\n",
// Setext-style & 4 indents
" 0.1.0\n==\n",
// Setext-style & 4 indents
" 0.1.0\n--\n",
// Setext-style & non-'=' char
"0.1.0\n==?\n",
// Setext-style & non-'-' char
"0.1.0\n--?\n",
];
for changelog in &changelogs {
let e = parse(changelog).unwrap_err();
assert!(e.is_parse());
assert!(!e.is_format());
}
assert!(Parser::new().prefix_format("").is_ok());
assert!(Parser::new().prefix_format(" ").is_ok());
assert!(Parser::new().prefix_format("\t\n").is_ok());
assert!(Parser::new().prefix_format(r"\/").is_ok());
let e = Parser::new().version_format("").unwrap_err();
assert!(e.is_format());
assert_eq!(e.to_string(), "empty or whitespace version format");
assert!(!e.is_parse());
assert!(e.source().is_none());
let e = Parser::new().version_format(" ").unwrap_err();
assert!(e.is_format());
assert_eq!(e.to_string(), "empty or whitespace version format");
assert!(!e.is_parse());
assert!(e.source().is_none());
let e = Parser::new().version_format("\t\n").unwrap_err();
assert!(e.is_format());
assert_eq!(e.to_string(), "empty or whitespace version format");
assert!(!e.is_parse());
assert!(e.source().is_none());
let e = Parser::new().version_format("\\").unwrap_err();
assert!(e.is_format());
assert!(!e.is_parse());
assert!(e.source().is_some());
assert!(Parser::new().version_format(r"\/").is_ok());
}
#[test]
fn multiple_heading() {
let changelogs = ["## 0.1.0\n## 0.1.0\n", "## 0.1.0\n## 0.1.0\n## 0.0.0\n"];
for changelog in &changelogs {
let e = parse(changelog).unwrap_err();
assert!(e.is_parse());
assert!(!e.is_format());
}
let changelogs =
["## 0.1.0\n##0.1.0\n", "##0.1.0\n## 0.1.0\n##0.0.0\n", "##0.1.0\n## 0.1.0\n## other"];
for changelog in &changelogs {
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 1);
assert_eq!(changelog[0].title, "0.1.0");
}
}
#[test]
fn multiple_level() {
let changelogs = [
("### 0.2.0\na\n## 0.1.0\nb\n", "a"),
("## 0.2.0\na\n### 0.1.0\nb\n", "a\n### 0.1.0\nb"),
("## 0.2.0\na\n### 0.1.0", "a\n### 0.1.0"),
("## 0.2.0\na\n# 0.1.0", "a"),
("## 0.2.0\na\n## other\n# 0.1.0", "a"),
("## 0.2.0\na\n## other\n# 0.1.0\n", "a"),
];
for &(changelog, notes) in &changelogs {
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 1);
assert_eq!(changelog[0].title, "0.2.0");
assert_eq!(changelog[0].notes, notes);
}
let changelogs = [("## 0.2.0\na\n## other\n# 0.1.0\n## 0.1.0", "a")];
for &(changelog, notes) in &changelogs {
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 2);
assert_eq!(changelog[0].title, "0.2.0");
assert_eq!(changelog[0].notes, notes);
assert_eq!(changelog[1].title, "0.1.0");
assert_eq!(changelog[1].notes, "");
}
}
// Atx-style heading
#[test]
fn atx_heading() {
for level in 1..=6 {
let changelog = &format!("{} 0.1.0", "#".repeat(level));
assert_eq!(1, parse(changelog).unwrap().len());
}
let changelog = &format!("{} 0.1.0", "#".repeat(7));
let e = parse(changelog).unwrap_err();
assert!(e.is_parse());
assert!(!e.is_format());
}
#[test]
fn code_block() {
for fence in ["```", "```", "~~~", "~~~~"] {
let changelog = format!(
"\
# 0.2.0
{fence}
# 0.2.0
{fence}
# 0.1.0
{fence}
# 0.1.0
{fence}
"
);
let changelog = parse(&changelog).unwrap();
assert_eq!(changelog.len(), 2);
assert_eq!(changelog["0.2.0"].notes, format!("{fence}\n# 0.2.0\n{fence}"));
assert_eq!(changelog["0.1.0"].notes, format!("{fence}\n# 0.1.0\n{fence}"));
}
// https://pandoc.org/try/?params=%7B%22text%22%3A%22%60%60%60%5Cn%60%60%60%60%5Cna%5Cn%60%60%60%60%5Cn%60%60%60%5Cn%60%60%60%5Cn%7E%7E%7E%7E%5Cna%5Cn%7E%7E%7E%7E%5Cn%60%60%60%22%2C%22to%22%3A%22html5%22%2C%22from%22%3A%22commonmark%22%2C%22standalone%22%3Afalse%2C%22embed-resources%22%3Afalse%2C%22table-of-contents%22%3Afalse%2C%22number-sections%22%3Afalse%2C%22citeproc%22%3Afalse%2C%22html-math-method%22%3A%22plain%22%2C%22wrap%22%3A%22auto%22%2C%22highlight-style%22%3Anull%2C%22files%22%3A%7B%7D%2C%22template%22%3Anull%7D
let changelog = "\
# 0.2.0
```
# 0.2.0
````
# 0.1.0
````
# 0.1.0
```
# 0.1.0
```
# 0.1.0
~~~~
# 0.1.0
~~~~
# 0.1.0
```
# 0.1.0
";
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 2);
assert_eq!(changelog["0.2.0"].notes, "```\n# 0.2.0\n````");
assert_eq!(
changelog["0.1.0"].notes,
"````\n# 0.1.0\n```\n# 0.1.0\n```\n# 0.1.0\n~~~~\n# 0.1.0\n~~~~\n# 0.1.0\n```\n# 0.1.0"
);
let changelog = "\
# 0.2.0
# 0.2.0
# 0.1.0
# 0.1.0
";
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 2);
assert_eq!(changelog["0.2.0"].notes, " # 0.2.0");
assert_eq!(changelog["0.1.0"].notes, " # 0.1.0");
}
#[test]
fn comment() {
let changelog = "\
# 0.2.0
<!--
# 0.2.0
-->
# 0.1.0
<!--
# 0.1.0
-->
";
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 2);
assert_eq!(changelog["0.2.0"].notes, "<!--\n# 0.2.0\n-->");
assert_eq!(changelog["0.1.0"].notes, "<!--\n# 0.1.0\n-->");
let changelog = "\
# 0.2.0
<!--
# 0.2.0-->
# 0.1.0
<!--
# 0.1.0-->
";
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 2);
assert_eq!(changelog["0.2.0"].notes, "<!--\n# 0.2.0-->");
assert_eq!(changelog["0.1.0"].notes, "<!--\n# 0.1.0-->");
let changelog = "\
# 0.2.0
<!--
# 0.2.0 --> <!--
# 0.2.0 -->
# 0.1.0
<!--
# 0.1.0 <!-- -->
a
# 0.0.2 -->
# 0.0.1 <!-- -->
# 0.0.0 <!--
a
-->
";
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 5);
assert_eq!(changelog["0.2.0"].title, "0.2.0");
assert_eq!(changelog["0.2.0"].notes, "<!--\n# 0.2.0 --> <!--\n# 0.2.0 -->");
assert_eq!(changelog["0.1.0"].title, "0.1.0");
assert_eq!(changelog["0.1.0"].notes, "<!--\n# 0.1.0 <!-- -->\na");
assert_eq!(changelog["0.0.2"].title, "0.0.2 -->");
assert_eq!(changelog["0.0.2"].notes, "");
// TODO: option like title_no_link to remove comment from title
assert_eq!(changelog["0.0.1"].title, "0.0.1 <!-- -->");
assert_eq!(changelog["0.0.1"].notes, "");
// https://pandoc.org/try/?params=%7B%22text%22%3A%22%23+0.0.0+%3C%21--%5Cna%5Cn--%3E%22%2C%22to%22%3A%22html5%22%2C%22from%22%3A%22commonmark%22%2C%22standalone%22%3Afalse%2C%22embed-resources%22%3Afalse%2C%22table-of-contents%22%3Afalse%2C%22number-sections%22%3Afalse%2C%22citeproc%22%3Afalse%2C%22html-math-method%22%3A%22plain%22%2C%22wrap%22%3A%22auto%22%2C%22highlight-style%22%3Anull%2C%22files%22%3A%7B%7D%2C%22template%22%3Anull%7D
assert_eq!(changelog["0.0.0"].title, "0.0.0 <!--");
assert_eq!(changelog["0.0.0"].notes, "a\n-->");
}
#[test]
fn link() {
let changelog = "\
# [Version 0.5.2 2022-01-01]
# [Version 0.5.1 2022-01-01][link]
# [Version 0.5.0 2022-01-01](link)
# Version [0.4.2 2022-01-01]
# Version [0.4.1 2022-01-01][link]
# Version [0.4.0 2022-01-01](link)
# [0.3.2 2022-01-01]
# [0.3.1 2022-01-01][link]
# [0.3.0 2022-01-01](link)
# [0.2.2] 2022-01-01
# [0.2.1][link] [2022-01-01](link2)
# [0.2.0](link) 2022-[01-01][link2]
# [0.1.2]
# [0.1.1][link]
# [0.1.0](link)
";
let changelog = parse(changelog).unwrap();
assert_eq!(changelog.len(), 15);
assert_eq!(changelog["0.5.2"].version, "0.5.2");
assert_eq!(changelog["0.5.2"].title, "[Version 0.5.2 2022-01-01]");
assert_eq!(changelog["0.5.2"].title_no_link(), "Version 0.5.2 2022-01-01");
assert_eq!(changelog["0.5.1"].version, "0.5.1");
assert_eq!(changelog["0.5.1"].title, "[Version 0.5.1 2022-01-01][link]");
assert_eq!(changelog["0.5.1"].title_no_link(), "Version 0.5.1 2022-01-01");
assert_eq!(changelog["0.5.0"].version, "0.5.0");
assert_eq!(changelog["0.5.0"].title, "[Version 0.5.0 2022-01-01](link)");
assert_eq!(changelog["0.5.0"].title_no_link(), "Version 0.5.0 2022-01-01");
assert_eq!(changelog["0.4.2"].version, "0.4.2");
assert_eq!(changelog["0.4.2"].title, "Version [0.4.2 2022-01-01]");
assert_eq!(changelog["0.4.2"].title_no_link(), "Version 0.4.2 2022-01-01");
assert_eq!(changelog["0.4.1"].version, "0.4.1");
assert_eq!(changelog["0.4.1"].title, "Version [0.4.1 2022-01-01][link]");
assert_eq!(changelog["0.4.1"].title_no_link(), "Version 0.4.1 2022-01-01");
assert_eq!(changelog["0.4.0"].version, "0.4.0");
assert_eq!(changelog["0.4.0"].title, "Version [0.4.0 2022-01-01](link)");
assert_eq!(changelog["0.4.0"].title_no_link(), "Version 0.4.0 2022-01-01");
assert_eq!(changelog["0.3.2"].version, "0.3.2");
assert_eq!(changelog["0.3.2"].title, "[0.3.2 2022-01-01]");
assert_eq!(changelog["0.3.2"].title_no_link(), "0.3.2 2022-01-01");
assert_eq!(changelog["0.3.1"].version, "0.3.1");
assert_eq!(changelog["0.3.1"].title, "[0.3.1 2022-01-01][link]");
assert_eq!(changelog["0.3.1"].title_no_link(), "0.3.1 2022-01-01");
assert_eq!(changelog["0.3.0"].version, "0.3.0");
assert_eq!(changelog["0.3.0"].title, "[0.3.0 2022-01-01](link)");
assert_eq!(changelog["0.3.0"].title_no_link(), "0.3.0 2022-01-01");
assert_eq!(changelog["0.2.2"].version, "0.2.2");
assert_eq!(changelog["0.2.2"].title, "[0.2.2] 2022-01-01");
assert_eq!(changelog["0.2.2"].title_no_link(), "0.2.2 2022-01-01");
assert_eq!(changelog["0.2.1"].version, "0.2.1");
assert_eq!(changelog["0.2.1"].title, "[0.2.1][link] [2022-01-01](link2)");
assert_eq!(changelog["0.2.1"].title_no_link(), "0.2.1 2022-01-01");
assert_eq!(changelog["0.2.0"].version, "0.2.0");
assert_eq!(changelog["0.2.0"].title, "[0.2.0](link) 2022-[01-01][link2]");
assert_eq!(changelog["0.2.0"].title_no_link(), "0.2.0 2022-01-01");
assert_eq!(changelog["0.1.2"].version, "0.1.2");
assert_eq!(changelog["0.1.2"].title, "[0.1.2]");
assert_eq!(changelog["0.1.2"].title_no_link(), "0.1.2");
assert_eq!(changelog["0.1.1"].version, "0.1.1");
assert_eq!(changelog["0.1.1"].title, "[0.1.1][link]");
assert_eq!(changelog["0.1.1"].title_no_link(), "0.1.1");
assert_eq!(changelog["0.1.0"].version, "0.1.0");
assert_eq!(changelog["0.1.0"].title, "[0.1.0](link)");
assert_eq!(changelog["0.1.0"].title_no_link(), "0.1.0");
}
#[test]
#[cfg_attr(miri, ignore)] // Miri is too slow
fn pin_project() {
let text = &fs::read_to_string(fixtures_dir().join("pin-project.md")).unwrap();
let changelog = parse(text).unwrap();
assert_eq!(changelog.len(), 82);
assert_diff(fixtures_dir().join("pin-project-1.0.0.md"), changelog["1.0.0"].notes);
// empty prefix format
let changelog = Parser::new().prefix_format("").unwrap().parse(text).unwrap();
assert_eq!(changelog.len(), 82);
assert_diff(fixtures_dir().join("pin-project-1.0.0.md"), changelog["1.0.0"].notes);
}
#[test]
#[cfg_attr(miri, ignore)] // Miri is too slow
fn rust() {
let text = &fs::read_to_string(fixtures_dir().join("rust.md")).unwrap();
let map = parse(text).unwrap();
assert_eq!(map.len(), 117);
assert_diff(fixtures_dir().join("rust-1.46.0.md"), map["1.46.0"].notes);
let vec: Vec<_> = parse_iter(text).collect();
assert_eq!(vec.len(), map.len());
assert_eq!(vec[47], map["1.46.0"]);
let text = &fs::read_to_string(fixtures_dir().join("rust-atx.md")).unwrap();
let map = parse(text).unwrap();
assert_eq!(map.len(), 117);
assert_diff(fixtures_dir().join("rust-1.46.0-atx.md"), map["1.46.0"].notes);
let vec: Vec<_> = parse_iter(text).collect();
assert_eq!(vec.len(), map.len());
assert_eq!(vec[47], map["1.46.0"]);
}
#[test]
#[cfg_attr(miri, ignore)] // Miri is too slow
fn cargo() {
let text = &fs::read_to_string(fixtures_dir().join("cargo.md")).unwrap();
let changelog = Parser::new()
.prefix_format("Cargo ")
.unwrap()
.version_format(r"^[0-9]+\.[0-9]+(\.[0-9])?$")
.unwrap()
.parse(text)
.unwrap();
assert_eq!(changelog.len(), 54);
assert_diff(fixtures_dir().join("cargo-1.50.md"), changelog["1.50"].notes);
assert_diff(fixtures_dir().join("cargo-1.77.1.md"), changelog["1.77.1"].notes);
}
// Regression tests for bugs caught by fuzzing.
#[test]
fn fuzz() {
let tests: &[(&str, Result<usize, &str>)] =
&[("1115.8.8 '9.\n-\n\u{c}\"----\u{19}\u{1f}<!--.4\n## 444.444.4\r\r \u{b}---->", Ok(1))];
for &(test, expected_len) in tests {
let res = parse(test);
match expected_len {
Ok(expected_len) => {
assert_eq!(res.unwrap().len(), expected_len);
}
Err(s) => {
assert_eq!(res.unwrap_err().to_string(), s);
}
}
}
}
#[test]
#[cfg_attr(miri, ignore)] // Miri is too slow
fn pathological_fake_heading() {
let text = &"#".repeat(1024 * 1024 * 128);
let now = std::time::Instant::now();
parse(text).unwrap_err();
eprintln!("{:?}", now.elapsed());
}