Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e133724

Browse files
committedAug 26, 2024·
fix parsing of compilation database; add rate limit tests
1 parent dd6fbe6 commit e133724

File tree

7 files changed

+133
-66
lines changed

7 files changed

+133
-66
lines changed
 

‎cpp-linter-lib/src/clang_tools/clang_tidy.rs

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@ use crate::{
1818
common_fs::{normalize_path, FileObj},
1919
};
2020

21-
/// Used to deserialize a JSON compilation database
22-
#[derive(Deserialize, Debug, Clone)]
23-
pub struct CompilationDatabase {
24-
/// A list of [`CompilationUnit`]
25-
units: Vec<CompilationUnit>,
26-
}
21+
// /// Used to deserialize a JSON compilation database
22+
// #[derive(Deserialize, Debug, Clone)]
23+
// pub struct CompilationDatabase {
24+
// /// A list of [`CompilationUnit`]
25+
// units: Vec<CompilationUnit>,
26+
// }
2727

2828
/// Used to deserialize a json compilation database's translation unit.
2929
///
3030
/// The only purpose this serves is to normalize relative paths for build systems that
3131
/// use/need relative paths (ie ninja).
3232
#[derive(Deserialize, Debug, Clone)]
33-
struct CompilationUnit {
33+
pub struct CompilationUnit {
3434
/// The directory of the build environment
3535
directory: String,
3636

@@ -75,15 +75,14 @@ pub struct TidyNotification {
7575

7676
impl TidyNotification {
7777
pub fn diagnostic_link(&self) -> String {
78-
let ret_val = if let Some((category, name)) = self.diagnostic.split_once('-') {
79-
format!(
80-
"[{}](https://clang.llvm.org/extra/clang-tidy/checks/{category}/{name}.html)",
81-
self.diagnostic
82-
)
83-
} else {
84-
self.diagnostic.clone()
85-
};
86-
ret_val
78+
if self.diagnostic.starts_with("clang-diagnostic") {
79+
return self.diagnostic.clone();
80+
}
81+
let (category, name) = self.diagnostic.split_once('-').unwrap();
82+
format!(
83+
"[{}](https://clang.llvm.org/extra/clang-tidy/checks/{category}/{name}.html)",
84+
self.diagnostic
85+
)
8786
}
8887
}
8988

@@ -100,7 +99,7 @@ pub struct TidyAdvice {
10099
/// in the notifications.
101100
fn parse_tidy_output(
102101
tidy_stdout: &[u8],
103-
database_json: &Option<CompilationDatabase>,
102+
database_json: &Option<Vec<CompilationUnit>>,
104103
) -> Option<TidyAdvice> {
105104
let note_header = Regex::new(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$").unwrap();
106105
let mut notification = None;
@@ -114,29 +113,29 @@ fn parse_tidy_output(
114113

115114
// normalize the filename path and try to make it relative to the repo root
116115
let mut filename = PathBuf::from(&captured[1]);
117-
if filename.is_relative() {
118-
// if database was given try to use that first
119-
if let Some(db_json) = &database_json {
120-
let mut found_unit = false;
121-
for unit in &db_json.units {
122-
if unit.file == captured[0] {
123-
filename =
124-
normalize_path(&PathBuf::from_iter([&unit.directory, &unit.file]));
125-
found_unit = true;
126-
break;
127-
}
116+
// if database was given try to use that first
117+
if let Some(db_json) = &database_json {
118+
let mut found_unit = false;
119+
for unit in db_json {
120+
let unit_path =
121+
PathBuf::from_iter([unit.directory.as_str(), unit.file.as_str()]);
122+
if unit_path == filename {
123+
filename =
124+
normalize_path(&PathBuf::from_iter([&unit.directory, &unit.file]));
125+
found_unit = true;
126+
break;
128127
}
129-
if !found_unit {
130-
// file was not a named unit in the database;
131-
// try to normalize path as if relative to working directory.
132-
// NOTE: This shouldn't happen with a properly formed JSON database
133-
filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
134-
}
135-
} else {
136-
// still need to normalize the relative path despite missing database info.
137-
// let's assume the file is relative to current working directory.
128+
}
129+
if !found_unit {
130+
// file was not a named unit in the database;
131+
// try to normalize path as if relative to working directory.
132+
// NOTE: This shouldn't happen with a properly formed JSON database
138133
filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
139134
}
135+
} else {
136+
// still need to normalize the relative path despite missing database info.
137+
// let's assume the file is relative to current working directory.
138+
filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
140139
}
141140
assert!(filename.is_absolute());
142141
if filename.is_absolute() && filename.starts_with(&cur_dir) {
@@ -198,7 +197,7 @@ pub fn run_clang_tidy(
198197
lines_changed_only: &LinesChangedOnly,
199198
database: &Option<PathBuf>,
200199
extra_args: &Option<Vec<String>>,
201-
database_json: &Option<CompilationDatabase>,
200+
database_json: &Option<Vec<CompilationUnit>>,
202201
) -> Vec<(log::Level, std::string::String)> {
203202
let mut logs = vec![];
204203
let mut file = file.lock().unwrap();

‎cpp-linter-lib/src/clang_tools/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::{
2424
pub mod clang_format;
2525
use clang_format::run_clang_format;
2626
pub mod clang_tidy;
27-
use clang_tidy::{run_clang_tidy, CompilationDatabase};
27+
use clang_tidy::{run_clang_tidy, CompilationUnit};
2828

2929
/// Fetch the path to a clang tool by `name` (ie `"clang-tidy"` or `"clang-format"`) and
3030
/// `version`.
@@ -182,12 +182,12 @@ pub async fn capture_clang_tools_output(
182182

183183
// parse database (if provided) to match filenames when parsing clang-tidy's stdout
184184
if let Some(db_path) = &clang_params.database {
185-
if let Ok(db_str) = fs::read(db_path) {
185+
if let Ok(db_str) = fs::read(db_path.join("compile_commands.json")) {
186186
clang_params.database_json = Some(
187-
serde_json::from_str::<CompilationDatabase>(
187+
serde_json::from_str::<Vec<CompilationUnit>>(
188188
String::from_utf8(db_str).unwrap().as_str(),
189189
)
190-
.unwrap(),
190+
.expect("Failed to parse compile_commands.json"),
191191
)
192192
}
193193
};

‎cpp-linter-lib/src/cli/structs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::PathBuf;
33
use clap::ArgMatches;
44

55
use super::convert_extra_arg_val;
6-
use crate::{clang_tools::clang_tidy::CompilationDatabase, common_fs::FileFilter};
6+
use crate::{clang_tools::clang_tidy::CompilationUnit, common_fs::FileFilter};
77

88
/// An enum to describe `--lines-changed-only` CLI option's behavior.
99
#[derive(PartialEq, Clone, Debug)]
@@ -139,7 +139,7 @@ pub struct ClangParams {
139139
pub lines_changed_only: LinesChangedOnly,
140140
pub database: Option<PathBuf>,
141141
pub extra_args: Option<Vec<String>>,
142-
pub database_json: Option<CompilationDatabase>,
142+
pub database_json: Option<Vec<CompilationUnit>>,
143143
pub style: String,
144144
pub clang_tidy_command: Option<PathBuf>,
145145
pub clang_format_command: Option<PathBuf>,

‎cpp-linter-lib/src/rest_api/github_api.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,6 @@ impl GithubApiClient {
361361
#[allow(clippy::nonminimal_bool)] // an inaccurate assessment
362362
if (is_lgtm && !no_lgtm) || !is_lgtm {
363363
let payload = HashMap::from([("body", comment.to_owned())]);
364-
#[cfg(not(test))]
365364
log::debug!("payload body:\n{:?}", payload);
366365
let req_meth = if comment_url.is_some() {
367366
Method::PATCH
@@ -529,7 +528,10 @@ mod test {
529528
sync::{Arc, Mutex},
530529
};
531530

531+
use chrono::Utc;
532+
use mockito::{Matcher, Server};
532533
use regex::Regex;
534+
use reqwest::{Method, Url};
533535
use tempfile::{tempdir, NamedTempFile};
534536

535537
use super::GithubApiClient;
@@ -619,4 +621,49 @@ mod test {
619621
"checks-failed=0\nformat-checks-failed=0\ntidy-checks-failed=0\n"
620622
);
621623
}
624+
625+
async fn simulate_rate_limit(secondary: bool) {
626+
let mut server = Server::new_async().await;
627+
let url = Url::parse(server.url().as_str()).unwrap();
628+
env::set_var("GITHUB_API_URL", server.url());
629+
let client = GithubApiClient::default();
630+
let reset_timestamp = (Utc::now().timestamp() + 60).to_string();
631+
let mock = server
632+
.mock("GET", "/")
633+
.match_query(Matcher::Any)
634+
.with_status(429)
635+
.with_header(
636+
&client.rate_limit_headers.remaining,
637+
if secondary { "1" } else { "0" },
638+
)
639+
.with_header(&client.rate_limit_headers.reset, &reset_timestamp);
640+
if secondary {
641+
mock.with_header(&client.rate_limit_headers.retry, "0")
642+
.create();
643+
} else {
644+
mock.create();
645+
}
646+
let request =
647+
GithubApiClient::make_api_request(&client.client, url, Method::GET, None, None);
648+
let _response = GithubApiClient::send_api_request(
649+
client.client.clone(),
650+
request,
651+
true,
652+
client.rate_limit_headers.clone(),
653+
0,
654+
)
655+
.await;
656+
}
657+
658+
#[tokio::test]
659+
#[should_panic(expected = "REST API secondary rate limit exceeded")]
660+
async fn secondary_rate_limit() {
661+
simulate_rate_limit(true).await;
662+
}
663+
664+
#[tokio::test]
665+
#[should_panic(expected = "REST API rate limit exceeded!")]
666+
async fn primary_rate_limit() {
667+
simulate_rate_limit(false).await;
668+
}
622669
}

‎cpp-linter-lib/src/rest_api/mod.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,24 +131,24 @@ pub trait RestApiClient {
131131
.expect("Failed to extract remaining attempts about rate limit")
132132
.parse::<i64>()
133133
.expect("Failed to parse i64 from remaining attempts about rate limit");
134-
let reset = DateTime::from_timestamp(
135-
response
136-
.headers()
137-
.get(&rate_limit_headers.reset)
138-
.expect("response headers does not include a reset timestamp")
139-
.to_str()
140-
.expect("Failed to extract reset info about rate limit")
141-
.parse::<i64>()
142-
.expect("Failed to parse i64 from reset time about rate limit"),
143-
0,
144-
)
145-
.expect("rate limit reset UTC timestamp is an invalid");
146134
if remaining <= 0 {
135+
let reset = DateTime::from_timestamp(
136+
response
137+
.headers()
138+
.get(&rate_limit_headers.reset)
139+
.expect("response headers does not include a reset timestamp")
140+
.to_str()
141+
.expect("Failed to extract reset info about rate limit")
142+
.parse::<i64>()
143+
.expect("Failed to parse i64 from reset time about rate limit"),
144+
0,
145+
)
146+
.expect("rate limit reset UTC timestamp is an invalid");
147147
panic!("REST API rate limit exceeded! Resets at {}", reset);
148148
}
149149

150150
// check if secondary rate limit is violated; backoff and try again.
151-
if retries >= 5 {
151+
if retries > 4 {
152152
panic!("REST API secondary rate limit exceeded");
153153
}
154154
if let Some(retry) = response.headers().get(&rate_limit_headers.retry) {

‎cpp-linter-lib/tests/comments.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
use chrono::Utc;
22
use cpp_linter_lib::run::run_main;
33
use mockito::{Matcher, Server, ServerGuard};
4-
use std::{env, fmt::Display, fs, io::Write, path::Path, process::Command};
4+
use std::{
5+
env,
6+
fmt::Display,
7+
fs,
8+
io::{self, BufRead, Write},
9+
path::Path,
10+
process::Command,
11+
};
512
use tempfile::{NamedTempFile, TempDir};
613

714
const SHA: &str = "8d68756375e0483c7ac2b4d6bbbece420dbbb495";
@@ -155,6 +162,8 @@ async fn setup(
155162
"--ignore-format=src/some source.c".to_string(),
156163
format!("--thread-comments={thread_comments}"),
157164
format!("--no-lgtm={no_lgtm}"),
165+
"-p=build".to_string(),
166+
"-i=build".to_string(),
158167
];
159168
if is_lgtm {
160169
args.push("-e=c".to_string());
@@ -176,9 +185,17 @@ fn create_test_space() -> TempDir {
176185
}
177186

178187
// generate compilation database with meson (& ninja)
179-
let test_dir = tmp.path().to_str().unwrap();
188+
let test_dir = tmp.path().join("src");
180189
let mut cmd = Command::new("meson");
181-
cmd.args(["init", "-C", test_dir]);
190+
cmd.args([
191+
"init",
192+
"-C",
193+
test_dir.to_str().unwrap(),
194+
"--name",
195+
"demo",
196+
"demo.cpp",
197+
"demo.hpp",
198+
]);
182199
let output = cmd.output().expect("Failed to run 'meson init'");
183200
println!(
184201
"meson init stdout:\n{}",
@@ -190,7 +207,7 @@ fn create_test_space() -> TempDir {
190207
"setup",
191208
"--backend=ninja",
192209
meson_build_dir.as_path().to_str().unwrap(),
193-
test_dir,
210+
test_dir.to_str().unwrap(),
194211
]);
195212
let output = cmd
196213
.output()
@@ -199,6 +216,11 @@ fn create_test_space() -> TempDir {
199216
"meson setup stdout:\n{}",
200217
String::from_utf8(output.stdout.to_vec()).unwrap()
201218
);
219+
let db = fs::File::open(meson_build_dir.join("compile_commands.json"))
220+
.expect("Failed to open compilation database");
221+
for line in io::BufReader::new(db).lines().map_while(Result::ok) {
222+
println!("{line}");
223+
}
202224
tmp
203225
}
204226

‎justfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,20 @@ py-dev:
2020
# run the test suite
2121
[group("code coverage")]
2222
test:
23-
cargo llvm-cov --no-report --tests \
23+
cargo llvm-cov --no-report \
2424
nextest --manifest-path cpp-linter-lib/Cargo.toml \
25-
#--success-output=final \
2625
--lib --color always
2726

2827
# generate and open pretty coverage report
2928
[group("code coverage")]
3029
pretty-cov *args='':
31-
cargo llvm-cov report --json --output-path coverage.json
30+
cargo llvm-cov report --json --output-path coverage.json --ignore-filename-regex main
3231
llvm-cov-pretty coverage.json {{ args }}
3332

3433
# generate and open detailed coverage report
3534
[group("code coverage")]
3635
llvm-cov *args='':
37-
cargo llvm-cov report --html {{ args }}
36+
cargo llvm-cov report --html --ignore-filename-regex main {{ args }}
3837

3938
# This is useful for IDE gutter indicators of line coverage.
4039
# See Coverage Gutters ext in VSCode.

0 commit comments

Comments
 (0)
Please sign in to comment.