• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xd009642 / llvm-profparser / #214

28 Sep 2025 05:30PM UTC coverage: 69.656% (+0.3%) from 69.394%
#214

push

web-flow
Test all supported LLVM versions at once (#58)

Previously, testing all supported configurations required installing
many different versions of rustc manually. Now, it only requires running
`cargo test --all-features`.

This introduces a dependency on `rustup`. It also introduces a
dependency on `cargo-binutils` with 166 merged. Note that PR has not yet
been merged at time of writing.

By default, this only tests LLVM 20, which is the latest version
currently available on stable rust.

45 of 49 new or added lines in 1 file covered. (91.84%)

2 existing lines in 1 file now uncovered.

1235 of 1773 relevant lines covered (69.66%)

3.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.74
/tests/profdata.rs
1
use llvm_profparser::{merge_profiles, parse, parse_bytes};
2
use serde::Deserialize;
3
use std::collections::{HashMap, HashSet};
4
use std::ffi::OsStr;
5
use std::fs::read_dir;
6
use std::io::BufRead as _;
7
use std::iter::FromIterator as _;
8
use std::path::PathBuf;
9
use std::process::Command;
10
use std::sync::LazyLock;
11

12
/*
13
Counters:
14
  simple_loops:
15
    Hash: 0x00046d109c4436d1
16
    Counters: 4
17
    Function count: 1
18
    Block counts: [100, 100, 75]
19

20
    Instrumentation level: Front-end
21
Functions shown: 12
22
Total functions: 12
23
Maximum function count: 1
24
Maximum internal block count: 100
25
 */
26

27
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
28
struct Output {
29
    #[serde(rename = "Counters", default)]
30
    counters: HashMap<String, Entry>,
31
    #[serde(rename = "Instrumentation level")]
32
    instrumentation_level: Option<String>,
33
    #[serde(rename = "Functions shown")]
34
    functions_shown: Option<usize>,
35
    #[serde(rename = "Total functions")]
36
    total_functions: Option<usize>,
37
    #[serde(rename = "Maximum function count")]
38
    maximum_function_count: Option<usize>,
39
    #[serde(rename = "Maximum internal block count")]
40
    maximum_internal_block_count: Option<usize>,
41
}
42

43
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
44
#[serde(rename_all = "PascalCase")]
45
struct Entry {
46
    hash: Option<usize>,
47
    counters: Option<usize>,
48
    #[serde(rename = "Function count")]
49
    function_count: Option<usize>,
50
    #[serde(rename = "Block counts", default)]
51
    block_counts: Vec<usize>,
52
}
53

54
fn data_root_dir() -> PathBuf {
3✔
55
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/profdata")
6✔
56
}
57

58
// map of { llvm: rustc } versions
59
static SUPPORTED_LLVM_VERSIONS: LazyLock<HashMap<u8, &str>> = LazyLock::new(|| {
1✔
60
    LazyLock::force(&ASSERT_CMDS_EXIST);
1✔
61

62
    let map = HashMap::from_iter([
1✔
63
        #[cfg(feature = "__llvm_11")]
64
        (11, "1.51"),
65
        #[cfg(feature = "__llvm_12")]
66
        (12, "1.55"),
67
        #[cfg(feature = "__llvm_13")]
68
        (13, "1.57"),
69
        #[cfg(feature = "__llvm_14")]
70
        (14, "1.64"),
71
        #[cfg(feature = "__llvm_15")]
72
        (15, "1.69"),
73
        #[cfg(feature = "__llvm_16")]
74
        (16, "1.72"),
75
        #[cfg(feature = "__llvm_17")]
76
        (17, "1.77"),
77
        #[cfg(feature = "__llvm_18")]
78
        (18, "1.81"),
79
        #[cfg(feature = "__llvm_19")]
80
        (19, "1.86"),
81
        #[cfg(feature = "__llvm_20")]
82
        (20, "1.90"),
1✔
83
        // TODO: pin this to 1.91 once it releases
84
        #[cfg(feature = "__llvm_21")]
85
        (21, "nightly-2025-09-07"),
86
    ]);
87

88
    // Install all the versions we care about.
89
    // TODO: this is slow :/ but adding a stamp file is non-trivial because it needs to account for
90
    // which rustc versions are present in the map.
91
    println!("installing {} rustc versions", map.len());
2✔
92
    let status = Command::new("rustup")
1✔
93
        .args(&[
1✔
94
            "install",
95
            "--profile=minimal",
96
            "--component=llvm-tools-preview",
97
            "--no-self-update",
98
        ])
99
        .args(map.values())
1✔
100
        .status();
1✔
NEW
101
    assert!(
×
102
        status.ok().is_some_and(|s| s.success()),
3✔
103
        "failed to install rustc versions"
104
    );
105

106
    map
1✔
107
});
108

109
static LATEST_SUPPORTED_VERSION: u8 = 21;
110

111
#[test]
112
// this test is 'heavy', since it downloads a new toolchain each day.
113
// make it opt-in with `cargo test -- --ignored`.
114
#[ignore]
115
fn latest_llvm_supported() {
116
    let status = Command::new("rustup")
117
        .args(&["update", "nightly", "--no-self-update"])
118
        .status();
119
    assert!(
120
        status.ok().is_some_and(|s| s.success()),
121
        "failed to update nightly",
122
    );
123
    let rustc_vv = Command::new("rustc")
124
        .arg("+nightly")
125
        .arg("-vV")
126
        .output()
127
        .expect("failed to get rustc +nightly version")
128
        .stdout;
129
    let last_line = rustc_vv.lines().last().unwrap().unwrap();
130
    let llvm_v = last_line
131
        .split("LLVM version: ")
132
        .nth(1)
133
        .expect("no llvm version?");
134
    let llvm_major_v: u8 = llvm_v
135
        .split('.')
136
        .next()
137
        .expect("LLVM version format changed?")
138
        .parse()
139
        .expect("LLVM major version not a u8?");
140
    assert_eq!(llvm_major_v, LATEST_SUPPORTED_VERSION);
141
}
142

143
fn get_data_dir(llvm_version: u8) -> PathBuf {
1✔
144
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
11✔
145
        .join("tests")
146
        .join("data")
147
        .join("profdata")
148
        .join(format!("llvm-{llvm_version}"))
4✔
149
}
150

151
fn check_merge_command(files: &[PathBuf], id: &str, rustc_version: &str) {
1✔
152
    let llvm_output = PathBuf::from(format!("llvm_{}.profdata", id));
1✔
153
    let names = files
154
        .iter()
155
        .map(|x| x.display().to_string())
3✔
156
        .collect::<Vec<String>>();
157
    let llvm = Command::new("cargo")
1✔
158
        .args(&[&format!("+{rustc_version}"), "profdata", "--", "merge"])
3✔
159
        .args(&names)
1✔
160
        .arg("-o")
161
        .arg(&llvm_output)
1✔
162
        .output()
163
        .unwrap();
164

165
    if llvm.status.success() {
1✔
166
        let llvm_merged = parse(&llvm_output).unwrap();
2✔
167
        let rust_merged = merge_profiles(&names).unwrap();
2✔
168

169
        // Okay so we don't care about versioning. We don't care about symtab as there might be
170
        // hash collisions. And we don't care about the record ordering.
171
        assert_eq!(
1✔
172
            llvm_merged.is_ir_level_profile(),
2✔
173
            rust_merged.is_ir_level_profile()
1✔
174
        );
175
        assert_eq!(
1✔
176
            llvm_merged.has_csir_level_profile(),
1✔
177
            rust_merged.has_csir_level_profile()
1✔
178
        );
179
        let llvm_records = llvm_merged.records().iter().collect::<HashSet<_>>();
1✔
180
        let rust_records = rust_merged.records().iter().collect::<HashSet<_>>();
2✔
181
        assert!(!llvm_records.is_empty());
2✔
182
        std::assert_eq!(llvm_records, rust_records);
2✔
183
    } else {
184
        println!("Unsupported LLVM version");
×
185
    }
186
}
187

188
// Check that we have the exes we need to run these test at all.
189
// Otherwise, we give very poor error messages running the same commands in a loop over and over.
190
static ASSERT_CMDS_EXIST: LazyLock<()> = LazyLock::new(|| {
1✔
191
    assert_cmd::Command::new("cargo")
1✔
192
        .args(&["profdata", "--version"])
1✔
193
        .assert()
1✔
194
        .append_context(
1✔
195
            "help",
196
            "run 'cargo install cargo-binutils && rustup component add llvm-tools-preview'",
197
        )
198
        .success();
1✔
199
    // this is the version of llvm-profdata itself
200
    assert_cmd::Command::new("cargo")
1✔
201
        .args(&["profdata", "--", "--version"])
1✔
202
        .assert()
1✔
203
        .append_context("help", "run 'rustup component add llvm-tools-preview'")
1✔
204
        .success();
1✔
205
});
206

207
fn check_command(ext: &OsStr, llvm_version: u8) {
2✔
208
    // TODO we should consider doing different permutations of args. Some things which rely on
209
    // the ordering of elements in a priority_queue etc will display differently though...
210
    let data = get_data_dir(llvm_version);
2✔
211
    let rustc_version = SUPPORTED_LLVM_VERSIONS
4✔
212
        .get(&llvm_version)
2✔
213
        .expect("unsupported llvm version?");
214
    println!("Data directory: {}", data.display());
2✔
215
    let mut count = 0;
2✔
216
    for raw_file in read_dir(&data)
6✔
217
        .unwrap()
2✔
218
        .filter_map(|x| x.ok())
6✔
219
        .filter(|x| x.path().extension().unwrap_or_default() == ext)
6✔
220
    {
221
        println!("{:?}", raw_file.file_name());
4✔
222
        // llvm-profdata won't be able to work on all the files as it depends on what the host OS
223
        // llvm comes with by default. So first we check if it works and if so we test
224
        let llvm = Command::new("cargo")
2✔
225
            .current_dir(&data)
2✔
226
            .args(&[
1✔
227
                &format!("+{rustc_version}"),
2✔
228
                "profdata",
2✔
229
                "--",
230
                "show",
231
                "--all-functions",
232
                "--counts",
233
            ])
234
            .arg(raw_file.file_name())
1✔
235
            .output()
236
            .expect("cargo not installed???");
237

238
        let llvm_struct: Output = serde_yaml::from_slice(&llvm.stdout).unwrap();
1✔
239

240
        if llvm.status.success() {
2✔
241
            println!("Checking {:?}", raw_file.file_name());
2✔
242
            count += 1;
1✔
243
            let rust = assert_cmd::Command::cargo_bin("profparser")
1✔
244
                .unwrap()
245
                .current_dir(&data)
1✔
246
                .args(&["show", "--all-functions", "--counts", "-i"])
1✔
247
                .arg(raw_file.file_name())
1✔
248
                .output()
249
                .expect("Failed to run profparser on file");
250
            println!("{}", String::from_utf8_lossy(&rust.stderr));
1✔
251

252
            let mut rust_struct: Output = serde_yaml::from_slice(&rust.stdout).unwrap();
1✔
253
            if llvm_version == 11
1✔
NEW
254
                && rust_struct.instrumentation_level == Some("IR  entry_first = 0".into())
×
255
            {
NEW
256
                rust_struct.instrumentation_level = Some("IR".into());
×
257
            }
258

259
            assert_eq!(rust_struct, llvm_struct);
2✔
260
        } else {
261
            println!(
1✔
262
                "LLVM tools failed:\n{}",
263
                String::from_utf8_lossy(&llvm.stderr)
2✔
264
            );
265
        }
266
    }
267
    if count == 0 {
1✔
NEW
268
        panic!("No tests for LLVM version {}", llvm_version);
×
269
    }
270
}
271

272
fn check_against_text(ext: &OsStr, llvm_version: u8) {
1✔
273
    let data = get_data_dir(llvm_version);
1✔
274
    let rustc_version = SUPPORTED_LLVM_VERSIONS
2✔
275
        .get(&llvm_version)
1✔
276
        .expect("unsupported llvm version?");
277

278
    let mut count = 0;
1✔
279
    for raw_file in read_dir(&data)
3✔
280
        .unwrap()
1✔
281
        .filter_map(|x| x.ok())
3✔
282
        .filter(|x| x.path().extension().unwrap_or_default() == ext)
3✔
283
    {
284
        println!("{:?}", raw_file.file_name());
2✔
285
        let llvm = Command::new("cargo")
1✔
286
            .current_dir(&data)
1✔
287
            .args(&[
1✔
288
                &format!("+{rustc_version}"),
1✔
289
                "profdata",
1✔
290
                "--",
291
                "show",
292
                "--text",
293
                "--all-functions",
294
                "--counts",
295
            ])
296
            .arg(raw_file.file_name())
1✔
297
            .output()
298
            .expect("failed to spawn cargo?");
299

300
        if llvm.status.success() {
1✔
301
            count += 1;
2✔
302
            println!(
1✔
303
                "Parsing file: {}",
304
                data.join(raw_file.file_name()).display()
3✔
305
            );
306
            println!("{}", String::from_utf8_lossy(&llvm.stdout));
1✔
307
            let text_prof = parse_bytes(&llvm.stdout).unwrap();
1✔
308
            let parsed_prof = parse(data.join(raw_file.file_name())).unwrap();
2✔
309

310
            // Okay so we don't care about versioning. We don't care about symtab as there might be
311
            // hash collisions. And we don't care about the record ordering.
312

313
            assert_eq!(
1✔
314
                text_prof.is_ir_level_profile(),
2✔
315
                parsed_prof.is_ir_level_profile()
1✔
316
            );
317
            assert_eq!(
1✔
318
                text_prof.has_csir_level_profile(),
1✔
319
                parsed_prof.has_csir_level_profile()
1✔
320
            );
321
            let text_records = text_prof.records().iter().collect::<HashSet<_>>();
1✔
322
            let parse_records = parsed_prof.records().iter().collect::<HashSet<_>>();
2✔
323
            assert_eq!(text_records, parse_records);
2✔
324
        } else {
325
            println!(
1✔
326
                "{} failed: {}",
327
                raw_file.path().display(),
3✔
328
                String::from_utf8_lossy(&llvm.stderr),
1✔
329
            );
330
        }
331
    }
332
    if count == 0 {
1✔
333
        panic!("No tests for this LLVM version");
×
334
    }
335
}
336

337
#[test]
338
fn show_profraws() {
3✔
339
    let ext = OsStr::new("profraw");
1✔
340
    for &llvm_version in SUPPORTED_LLVM_VERSIONS.keys() {
2✔
341
        println!("testing profraws for llvm version {llvm_version}");
1✔
342
        check_command(ext, llvm_version);
1✔
343
    }
344
}
345

346
#[test]
347
fn show_proftexts() {
3✔
348
    let ext = OsStr::new("proftext");
1✔
349
    for &llvm_version in SUPPORTED_LLVM_VERSIONS.keys() {
2✔
350
        println!("testing proftext for llvm version {llvm_version}");
1✔
351
        check_command(ext, llvm_version);
1✔
352
    }
353
}
354

355
#[test]
356
fn show_profdatas() {
3✔
357
    let ext = OsStr::new("profdata");
1✔
358
    // Ordering of elements in printout make most of these tests troublesome
359
    for &llvm_version in SUPPORTED_LLVM_VERSIONS.keys() {
2✔
360
        check_against_text(ext, llvm_version);
1✔
361
    }
362
}
363

364
#[test]
365
fn merge() {
3✔
366
    for (llvm_version, rustc_version) in &*SUPPORTED_LLVM_VERSIONS {
2✔
367
        let data = get_data_dir(*llvm_version);
1✔
368
        let files = [
1✔
369
            data.join("foo3bar3-1.proftext"),
2✔
370
            data.join("foo3-1.proftext"),
2✔
371
            data.join("foo3-2.proftext"),
2✔
372
        ];
373
        check_merge_command(&files, "foo_results", rustc_version);
1✔
374
    }
375
}
376

377
#[test]
378
fn multi_app_profraw_merging() {
3✔
379
    let premerge_1 = data_root_dir()
2✔
380
        .join("misc")
381
        .join("multibin_merge/bin_1.profraw");
382
    let premerge_2 = data_root_dir()
2✔
383
        .join("misc")
384
        .join("multibin_merge/bin_2.1.profraw");
385
    let premerge_3 = data_root_dir()
2✔
386
        .join("misc")
387
        .join("multibin_merge/bin_2.2.profraw");
388
    let premerge_4 = data_root_dir()
2✔
389
        .join("misc")
390
        .join("multibin_merge/bin_2.3.profraw");
391

392
    let merged = merge_profiles(&[
1✔
393
        premerge_1.clone(),
1✔
394
        premerge_2.clone(),
1✔
395
        premerge_3.clone(),
1✔
396
        premerge_4.clone(),
1✔
397
    ])
398
    .unwrap();
399

400
    let profraw = parse(&premerge_1).unwrap();
1✔
401
    for (hash, name) in profraw.symtab.iter() {
2✔
402
        assert_eq!(merged.symtab.get(*hash), Some(name));
2✔
403
    }
404

405
    let profraw = parse(&premerge_2).unwrap();
1✔
406
    for (hash, name) in profraw.symtab.iter() {
2✔
407
        assert_eq!(merged.symtab.get(*hash), Some(name));
2✔
408
    }
409

410
    let profraw = parse(&premerge_3).unwrap();
1✔
411
    for (hash, name) in profraw.symtab.iter() {
2✔
412
        assert_eq!(merged.symtab.get(*hash), Some(name));
2✔
413
    }
414

415
    let profraw = parse(&premerge_4).unwrap();
1✔
416
    for (hash, name) in profraw.symtab.iter() {
2✔
417
        assert_eq!(merged.symtab.get(*hash), Some(name));
2✔
418
    }
419
}
420

421
#[test]
422
fn profraw_merging() {
3✔
423
    let premerge_1 = data_root_dir().join("misc").join("premerge_1.profraw");
1✔
424
    let premerge_2 = data_root_dir().join("misc").join("premerge_2.profraw");
1✔
425
    let merged = data_root_dir().join("misc").join("merged.profdata");
1✔
426

427
    let expected_merged = merge_profiles(&[merged]).unwrap();
1✔
428
    let merged = merge_profiles(&[premerge_1, premerge_2]).unwrap();
1✔
429

430
    assert_eq!(merged.symtab, expected_merged.symtab);
1✔
431
    assert_eq!(merged.records(), expected_merged.records());
1✔
432
}
433

434
#[test]
435
fn check_raw_data_consistency() {
3✔
436
    let raw = data_root_dir().join("misc").join("stable.profraw");
1✔
437
    let data = data_root_dir().join("misc").join("stable.profdata");
1✔
438

439
    let raw = merge_profiles(&[raw]).unwrap();
1✔
440
    let data = merge_profiles(&[data]).unwrap();
1✔
441

442
    // Merged with sparse so need to filter out some records
443
    for (hash, name) in data.symtab.iter() {
1✔
444
        println!("Seeing if {}:{} in Raw", hash, name);
2✔
445
        std::assert_eq!(name, raw.symtab.get(*hash).unwrap());
1✔
446

447
        let data_record = data.get_record(name);
1✔
448
        let raw_record = raw.get_record(name);
1✔
449
        std::assert_eq!(data_record, raw_record);
1✔
450
    }
451
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc