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

vigna / webgraph-rs / 19383593169

15 Nov 2025 03:16AM UTC coverage: 62.139% (-0.06%) from 62.201%
19383593169

push

github

web-flow
Merge pull request #156 from progval/edition2024

Switch webgraph to 2024 edition

9 of 19 new or added lines in 6 files covered. (47.37%)

7 existing lines in 3 files now uncovered.

5247 of 8444 relevant lines covered (62.14%)

29479360.02 hits per line

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

41.89
/cli/src/lib.rs
1
/*
2
 * SPDX-FileCopyrightText: 2023 Inria
3
 * SPDX-FileCopyrightText: 2023 Tommaso Fontana
4
 * SPDX-FileCopyrightText: 2025 Sebastiano Vigna
5
 *
6
 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
7
 */
8

9
#![doc = include_str!("../README.md")]
10
#![allow(clippy::type_complexity)]
11

12
use anyhow::{anyhow, bail, ensure, Context, Result};
13
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
14
use common_traits::{ToBytes, UnsignedInt};
15
use dsi_bitstream::dispatch::Codes;
16
use epserde::ser::Serialize;
17
use std::io::{BufWriter, Write};
18
use std::path::{Path, PathBuf};
19
use std::time::Duration;
20
use std::time::SystemTime;
21
use sux::bits::BitFieldVec;
22
use webgraph::prelude::CompFlags;
23
use webgraph::utils::{Granularity, MemoryUsage};
24

25
#[cfg(not(any(feature = "le_bins", feature = "be_bins")))]
26
compile_error!("At least one of the features `le_bins` or `be_bins` must be enabled.");
27

28
pub mod build_info {
29
    include!(concat!(env!("OUT_DIR"), "/built.rs"));
30

31
    pub fn version_string() -> String {
76✔
32
        format!(
76✔
33
            "{}
34
git info: {} {} {}
35
build info: built on {} for {} with {}",
36
            PKG_VERSION,
76✔
37
            GIT_VERSION.unwrap_or(""),
228✔
38
            GIT_COMMIT_HASH.unwrap_or(""),
228✔
39
            match GIT_DIRTY {
76✔
40
                None => "",
76✔
41
                Some(true) => "(dirty)",
×
42
                Some(false) => "(clean)",
×
43
            },
44
            BUILD_DATE,
45
            TARGET,
76✔
46
            RUSTC_VERSION
76✔
47
        )
48
    }
49
}
50

51
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
52
/// Enum for instantaneous codes.
53
///
54
/// It is used to implement [`ValueEnum`] here instead of in [`dsi_bitstream`].
55
pub enum PrivCode {
56
    Unary,
57
    Gamma,
58
    Delta,
59
    Zeta1,
60
    Zeta2,
61
    Zeta3,
62
    Zeta4,
63
    Zeta5,
64
    Zeta6,
65
    Zeta7,
66
}
67

68
impl From<PrivCode> for Codes {
69
    fn from(value: PrivCode) -> Self {
40✔
70
        match value {
40✔
71
            PrivCode::Unary => Codes::Unary,
8✔
72
            PrivCode::Gamma => Codes::Gamma,
24✔
73
            PrivCode::Delta => Codes::Delta,
×
74
            PrivCode::Zeta1 => Codes::Zeta { k: 1 },
75
            PrivCode::Zeta2 => Codes::Zeta { k: 2 },
76
            PrivCode::Zeta3 => Codes::Zeta { k: 3 },
77
            PrivCode::Zeta4 => Codes::Zeta { k: 4 },
78
            PrivCode::Zeta5 => Codes::Zeta { k: 5 },
79
            PrivCode::Zeta6 => Codes::Zeta { k: 6 },
80
            PrivCode::Zeta7 => Codes::Zeta { k: 7 },
81
        }
82
    }
83
}
84

85
#[derive(Args, Debug)]
86
/// Shared CLI arguments for reading files containing arcs.
87
pub struct ArcsArgs {
88
    #[arg(long, default_value_t = '#')]
89
    /// Ignore lines that start with this symbol.
90
    pub line_comment_symbol: char,
91

92
    #[arg(long, default_value_t = 0)]
93
    /// How many lines to skip, ignoring comment lines.
94
    pub lines_to_skip: usize,
95

96
    #[arg(long)]
97
    /// How many lines to parse, after skipping the first lines_to_skip and
98
    /// ignoring comment lines.
99
    pub max_arcs: Option<usize>,
100

101
    #[arg(long, default_value_t = '\t')]
102
    /// The column separator.
103
    pub separator: char,
104

105
    #[arg(long, default_value_t = 0)]
106
    /// The index of the column containing the source node of an arc.
107
    pub source_column: usize,
108

109
    #[arg(long, default_value_t = 1)]
110
    /// The index of the column containing the target node of an arc.
111
    pub target_column: usize,
112

113
    #[arg(long, default_value_t = false)]
114
    /// Source and destinations are not node identifiers starting from 0, but labels.
115
    pub labels: bool,
116
}
117

118
/// Parses the number of threads from a string.
119
///
120
/// This function is meant to be used with `#[arg(...,  value_parser =
121
/// num_threads_parser)]`.
122
pub fn num_threads_parser(arg: &str) -> Result<usize> {
12✔
123
    let num_threads = arg.parse::<usize>()?;
36✔
124
    ensure!(num_threads > 0, "Number of threads must be greater than 0");
24✔
125
    Ok(num_threads)
12✔
126
}
127

128
/// Shared CLI arguments for commands that specify a number of threads.
129
#[derive(Args, Debug)]
130
pub struct NumThreadsArg {
131
    #[arg(short = 'j', long, default_value_t = rayon::current_num_threads().max(1), value_parser = num_threads_parser)]
132
    /// The number of threads to use.
133
    pub num_threads: usize,
134
}
135

136
/// Shared CLI arguments for commands that specify a granularity.
137
#[derive(Args, Debug)]
138
pub struct GranularityArgs {
139
    #[arg(long, conflicts_with("node_granularity"))]
140
    /// The tentative number of arcs used to define the size of a parallel job
141
    /// (advanced option).
142
    pub arc_granularity: Option<u64>,
143

144
    #[arg(long, conflicts_with("arc_granularity"))]
145
    /// The tentative number of nodes used to define the size of a parallel job
146
    /// (advanced option).
147
    pub node_granularity: Option<usize>,
148
}
149

150
impl GranularityArgs {
151
    pub fn into_granularity(&self) -> Granularity {
4✔
152
        match (self.arc_granularity, self.node_granularity) {
8✔
153
            (Some(_), Some(_)) => unreachable!(),
154
            (Some(arc_granularity), None) => Granularity::Arcs(arc_granularity),
×
155
            (None, Some(node_granularity)) => Granularity::Nodes(node_granularity),
×
156
            (None, None) => Granularity::default(),
4✔
157
        }
158
    }
159
}
160

161
/// Shared CLI arguments for commands that specify a memory usage.
162
#[derive(Args, Debug)]
163
pub struct MemoryUsageArg {
164
    #[clap(short = 'm', long = "memory-usage", value_parser = memory_usage_parser, default_value = "50%")]
165
    /// The number of pairs to be used in batches.
166
    /// If the number ends with a `b` or `B` it is interpreted as a number of bytes, otherwise as a number of elements.
167
    /// You can use the SI and NIST multipliers k, M, G, T, P, ki, Mi, Gi, Ti, and Pi.
168
    /// You can also use a percentage of the available memory by appending a `%` to the number.
169
    pub memory_usage: MemoryUsage,
170
}
171

172
#[derive(Debug, Clone, Copy, ValueEnum)]
173
/// How to store vectors of floats.
174
pub enum FloatVectorFormat {
175
    /// Java-compatible format: a sequence of big-endian floats (32 or 64 bits).
176
    Java,
177
    /// A slice of floats (32 or 64 bits) serialized using ε-serde.
178
    Epserde,
179
    /// ASCII format, one float per line.
180
    Ascii,
181
    /// A JSON Array.
182
    Json,
183
}
184

185
impl FloatVectorFormat {
186
    /// Stores float values in the specified `path` using the format defined by
187
    /// `self`.
188
    ///
189
    /// If the result is a textual format, i.e., ASCII or JSON, `precision`
190
    /// will be used to truncate the float values to the specified number of
191
    /// decimal digits.
192
    pub fn store<F>(
×
193
        &self,
194
        path: impl AsRef<Path>,
195
        values: &[F],
196
        precision: Option<usize>,
197
    ) -> Result<()>
198
    where
199
        F: ToBytes + core::fmt::Display + epserde::ser::Serialize + Copy,
200
        for<'a> &'a [F]: epserde::ser::Serialize,
201
    {
202
        let precision = precision.unwrap_or(f64::DIGITS as usize);
×
203
        create_parent_dir(&path)?;
×
204
        let path_display = path.as_ref().display();
×
205
        let mut file = std::fs::File::create(&path)
×
206
            .with_context(|| format!("Could not create vector at {}", path_display))?;
×
207

208
        match self {
×
209
            FloatVectorFormat::Epserde => {
×
210
                log::info!("Storing in ε-serde format at {}", path_display);
×
211
                unsafe {
212
                    values
×
213
                        .serialize(&mut file)
×
214
                        .with_context(|| format!("Could not write vector to {}", path_display))
×
215
                }?;
216
            }
217
            FloatVectorFormat::Java => {
×
218
                log::info!("Storing in Java format at {}", path_display);
×
219
                for word in values.iter() {
×
220
                    file.write_all(word.to_be_bytes().as_ref())
×
221
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
222
                }
223
            }
224
            FloatVectorFormat::Ascii => {
×
225
                log::info!("Storing in ASCII format at {}", path_display);
×
226
                for word in values.iter() {
×
227
                    writeln!(file, "{word:.precision$}")
×
228
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
229
                }
230
            }
231
            FloatVectorFormat::Json => {
×
232
                log::info!("Storing in JSON format at {}", path_display);
×
233
                write!(file, "[")?;
×
234
                for word in values.iter().take(values.len().saturating_sub(2)) {
×
235
                    write!(file, "{word:.precision$}, ")
×
236
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
237
                }
238
                if let Some(last) = values.last() {
×
239
                    write!(file, "{last:.precision$}")
×
240
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
241
                }
242
                write!(file, "]")?;
×
243
            }
244
        }
245

246
        Ok(())
×
247
    }
248
}
249

250
#[derive(Debug, Clone, Copy, ValueEnum)]
251
/// How to store vectors of integers.
252
pub enum IntVectorFormat {
253
    /// Java-compatible format: a sequence of big-endian longs (64 bits).
254
    Java,
255
    /// A slice of usize serialized using ε-serde.
256
    Epserde,
257
    /// A BitFieldVec stored using ε-serde. It stores each element using
258
    /// ⌊log₂(max)⌋ + 1 bits. It requires to allocate the `BitFieldVec` in RAM
259
    /// before serializing it.
260
    BitFieldVec,
261
    /// ASCII format, one integer per line.
262
    Ascii,
263
    /// A JSON Array.
264
    Json,
265
}
266

267
impl IntVectorFormat {
268
    /// Stores a vector of `u64` in the specified `path`` using the format defined by `self`.
269
    ///
270
    /// `max` is the maximum value of the vector. If it is not provided, it will
271
    /// be computed from the data.
272
    pub fn store(&self, path: impl AsRef<Path>, data: &[u64], max: Option<u64>) -> Result<()> {
×
273
        // Ensure the parent directory exists
274
        create_parent_dir(&path)?;
×
275

276
        let mut file = std::fs::File::create(&path)
×
277
            .with_context(|| format!("Could not create vector at {}", path.as_ref().display()))?;
×
278
        let mut buf = BufWriter::new(&mut file);
×
279

280
        debug_assert_eq!(
×
281
            max,
×
282
            max.map(|_| { data.iter().copied().max().unwrap_or(0) }),
×
283
            "The wrong maximum value was provided for the vector"
×
284
        );
285

286
        match self {
×
287
            IntVectorFormat::Epserde => {
×
288
                log::info!("Storing in epserde format at {}", path.as_ref().display());
×
289
                unsafe {
290
                    data.serialize(&mut buf).with_context(|| {
×
291
                        format!("Could not write vector to {}", path.as_ref().display())
×
292
                    })
293
                }?;
294
            }
295
            IntVectorFormat::BitFieldVec => {
×
296
                log::info!(
×
297
                    "Storing in BitFieldVec format at {}",
×
298
                    path.as_ref().display()
×
299
                );
300
                let max = max.unwrap_or_else(|| {
×
301
                    data.iter()
×
302
                        .copied()
×
303
                        .max()
×
304
                        .unwrap_or_else(|| panic!("Empty vector"))
×
305
                });
306
                let bit_width = max.len() as usize;
×
307
                log::info!("Using {} bits per element", bit_width);
×
308
                let mut bit_field_vec = <BitFieldVec<u64, _>>::with_capacity(bit_width, data.len());
×
309
                bit_field_vec.extend(data.iter().copied());
×
310
                unsafe {
311
                    bit_field_vec.store(&path).with_context(|| {
×
312
                        format!("Could not write vector to {}", path.as_ref().display())
×
313
                    })
314
                }?;
315
            }
316
            IntVectorFormat::Java => {
×
317
                log::info!("Storing in Java format at {}", path.as_ref().display());
×
318
                for word in data.iter() {
×
319
                    buf.write_all(&word.to_be_bytes()).with_context(|| {
×
320
                        format!("Could not write vector to {}", path.as_ref().display())
×
321
                    })?;
322
                }
323
            }
324
            IntVectorFormat::Ascii => {
×
325
                log::info!("Storing in ASCII format at {}", path.as_ref().display());
×
326
                for word in data.iter() {
×
327
                    writeln!(buf, "{}", word).with_context(|| {
×
328
                        format!("Could not write vector to {}", path.as_ref().display())
×
329
                    })?;
330
                }
331
            }
332
            IntVectorFormat::Json => {
×
333
                log::info!("Storing in JSON format at {}", path.as_ref().display());
×
334
                write!(buf, "[")?;
×
335
                for word in data.iter().take(data.len().saturating_sub(2)) {
×
336
                    write!(buf, "{}, ", word).with_context(|| {
×
337
                        format!("Could not write vector to {}", path.as_ref().display())
×
338
                    })?;
339
                }
340
                if let Some(last) = data.last() {
×
341
                    write!(buf, "{}", last).with_context(|| {
×
342
                        format!("Could not write vector to {}", path.as_ref().display())
×
343
                    })?;
344
                }
345
                write!(buf, "]")?;
×
346
            }
347
        };
348

349
        Ok(())
×
350
    }
351

352
    #[cfg(target_pointer_width = "64")]
353
    /// Stores a vector of `usize` in the specified `path` using the format defined by `self`.
354
    /// `max` is the maximum value of the vector, if it is not provided, it will
355
    /// be computed from the data.
356
    ///
357
    /// This helper method is available only on 64-bit architectures as Java's format
358
    /// uses of 64-bit integers.
359
    pub fn store_usizes(
×
360
        &self,
361
        path: impl AsRef<Path>,
362
        data: &[usize],
363
        max: Option<usize>,
364
    ) -> Result<()> {
365
        self.store(
×
366
            path,
×
367
            unsafe { core::mem::transmute::<&[usize], &[u64]>(data) },
×
368
            max.map(|x| x as u64),
×
369
        )
370
    }
371
}
372

373
/// Parses a batch size.
374
///
375
/// This function accepts either a number (possibly followed by a
376
/// SI or NIST multiplier k, M, G, T, P, ki, Mi, Gi, Ti, or Pi), or a percentage
377
/// (followed by a `%`) that is interpreted as a percentage of the core
378
/// memory. If the value ends with a `b` or `B` it is interpreted as a number of
379
/// bytes, otherwise as a number of elements.
380
pub fn memory_usage_parser(arg: &str) -> anyhow::Result<MemoryUsage> {
8✔
381
    const PREF_SYMS: [(&str, u64); 10] = [
382
        ("k", 1E3 as u64),
383
        ("m", 1E6 as u64),
384
        ("g", 1E9 as u64),
385
        ("t", 1E12 as u64),
386
        ("p", 1E15 as u64),
387
        ("ki", 1 << 10),
388
        ("mi", 1 << 20),
389
        ("gi", 1 << 30),
390
        ("ti", 1 << 40),
391
        ("pi", 1 << 50),
392
    ];
393
    let arg = arg.trim().to_ascii_lowercase();
24✔
394
    ensure!(!arg.is_empty(), "empty string");
16✔
395

396
    if arg.ends_with('%') {
8✔
397
        let perc = arg[..arg.len() - 1].parse::<f64>()?;
32✔
398
        ensure!((0.0..=100.0).contains(&perc), "percentage out of range");
32✔
399
        return Ok(MemoryUsage::from_perc(perc));
8✔
400
    }
401

402
    let num_digits = arg
×
403
        .chars()
404
        .take_while(|c| c.is_ascii_digit() || *c == '.')
×
405
        .count();
406

407
    let number = arg[..num_digits].parse::<f64>()?;
×
408
    let suffix = &arg[num_digits..].trim();
×
409

410
    let multiplier = PREF_SYMS
×
411
        .iter()
412
        .find(|(x, _)| suffix.starts_with(x))
×
413
        .map(|(_, m)| m)
×
414
        .ok_or(anyhow!("invalid prefix symbol {}", suffix))?;
×
415

416
    let value = (number * (*multiplier as f64)) as usize;
×
417
    ensure!(value > 0, "batch size must be greater than zero");
×
418

419
    if suffix.ends_with('b') {
×
420
        Ok(MemoryUsage::MemorySize(value))
×
421
    } else {
422
        Ok(MemoryUsage::BatchSize(value))
×
423
    }
424
}
425

426
#[derive(Args, Debug)]
427
/// Shared CLI arguments for compression.
428
pub struct CompressArgs {
429
    /// The endianness of the graph to write
430
    #[clap(short = 'E', long)]
431
    pub endianness: Option<String>,
432

433
    /// The compression windows
434
    #[clap(short = 'w', long, default_value_t = 7)]
435
    pub compression_window: usize,
436
    /// The minimum interval length
437
    #[clap(short = 'i', long, default_value_t = 4)]
438
    pub min_interval_length: usize,
439
    /// The maximum recursion depth for references (-1 for infinite recursion depth)
440
    #[clap(short = 'r', long, default_value_t = 3)]
441
    pub max_ref_count: isize,
442

443
    #[arg(value_enum)]
444
    #[clap(long, default_value = "gamma")]
445
    /// The code to use for the outdegree
446
    pub outdegrees: PrivCode,
447

448
    #[arg(value_enum)]
449
    #[clap(long, default_value = "unary")]
450
    /// The code to use for the reference offsets
451
    pub references: PrivCode,
452

453
    #[arg(value_enum)]
454
    #[clap(long, default_value = "gamma")]
455
    /// The code to use for the blocks
456
    pub blocks: PrivCode,
457

458
    #[arg(value_enum)]
459
    #[clap(long, default_value = "zeta3")]
460
    /// The code to use for the residuals
461
    pub residuals: PrivCode,
462
}
463

464
impl From<CompressArgs> for CompFlags {
465
    fn from(value: CompressArgs) -> Self {
8✔
466
        CompFlags {
467
            outdegrees: value.outdegrees.into(),
16✔
468
            references: value.references.into(),
16✔
469
            blocks: value.blocks.into(),
16✔
470
            intervals: PrivCode::Gamma.into(),
16✔
471
            residuals: value.residuals.into(),
16✔
472
            min_interval_length: value.min_interval_length,
8✔
473
            compression_window: value.compression_window,
8✔
474
            max_ref_count: match value.max_ref_count {
8✔
475
                -1 => usize::MAX,
476
                _ => value.max_ref_count as usize,
477
            },
478
        }
479
    }
480
}
481

482
/// Creates a [`ThreadPool`](rayon::ThreadPool) with the given number of threads.
483
pub fn get_thread_pool(num_threads: usize) -> rayon::ThreadPool {
12✔
484
    rayon::ThreadPoolBuilder::new()
12✔
485
        .num_threads(num_threads)
24✔
486
        .build()
487
        .expect("Failed to create thread pool")
488
}
489

490
/// Appends a string to the filename of a path.
491
///
492
/// # Panics
493
/// * Will panic if there is no filename.
494
/// * Will panic in test mode if the path has an extension.
495
pub fn append(path: impl AsRef<Path>, s: impl AsRef<str>) -> PathBuf {
×
496
    debug_assert!(path.as_ref().extension().is_none());
×
497
    let mut path_buf = path.as_ref().to_owned();
×
498
    let mut filename = path_buf.file_name().unwrap().to_owned();
×
499
    filename.push(s.as_ref());
×
500
    path_buf.push(filename);
×
501
    path_buf
×
502
}
503

504
/// Creates all parent directories of the given file path.
505
pub fn create_parent_dir(file_path: impl AsRef<Path>) -> Result<()> {
20✔
506
    // ensure that the dst directory exists
507
    if let Some(parent_dir) = file_path.as_ref().parent() {
40✔
508
        std::fs::create_dir_all(parent_dir).with_context(|| {
60✔
509
            format!(
×
510
                "Failed to create the directory {:?}",
×
511
                parent_dir.to_string_lossy()
×
512
            )
513
        })?;
514
    }
515
    Ok(())
20✔
516
}
517

518
/// Parse a duration from a string.
519
/// For compatibility with Java, if no suffix is given, it is assumed to be in milliseconds.
520
/// You can use suffixes, the available ones are:
521
/// - `s` for seconds
522
/// - `m` for minutes
523
/// - `h` for hours
524
/// - `d` for days
525
///
526
/// Example: `1d2h3m4s567` this is parsed as: 1 day, 2 hours, 3 minutes, 4 seconds, and 567 milliseconds.
527
fn parse_duration(value: &str) -> Result<Duration> {
×
528
    if value.is_empty() {
×
529
        bail!("Empty duration string, if you want every 0 milliseconds use `0`.");
×
530
    }
531
    let mut duration = Duration::from_secs(0);
×
532
    let mut acc = String::new();
×
533
    for c in value.chars() {
×
534
        if c.is_ascii_digit() {
×
535
            acc.push(c);
×
536
        } else if c.is_whitespace() {
×
537
            continue;
×
538
        } else {
539
            let dur = acc.parse::<u64>()?;
×
540
            match c {
×
541
                's' => duration += Duration::from_secs(dur),
×
542
                'm' => duration += Duration::from_secs(dur * 60),
×
543
                'h' => duration += Duration::from_secs(dur * 60 * 60),
×
544
                'd' => duration += Duration::from_secs(dur * 60 * 60 * 24),
×
545
                _ => return Err(anyhow!("Invalid duration suffix: {}", c)),
×
546
            }
547
            acc.clear();
×
548
        }
549
    }
550
    if !acc.is_empty() {
×
551
        let dur = acc.parse::<u64>()?;
×
552
        duration += Duration::from_millis(dur);
×
553
    }
554
    Ok(duration)
×
555
}
556

557
/// Initializes the `env_logger` logger with a custom format including
558
/// timestamps with elapsed time since initialization.
559
pub fn init_env_logger() -> Result<()> {
4✔
560
    use jiff::fmt::friendly::{Designator, Spacing, SpanPrinter};
561
    use jiff::SpanRound;
562

563
    let mut builder =
4✔
564
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"));
12✔
565

566
    let start = std::time::Instant::now();
8✔
567
    let printer = SpanPrinter::new()
8✔
568
        .spacing(Spacing::None)
8✔
569
        .designator(Designator::Compact);
8✔
570
    let span_round = SpanRound::new()
8✔
571
        .largest(jiff::Unit::Day)
8✔
572
        .smallest(jiff::Unit::Millisecond)
8✔
573
        .days_are_24_hours();
574

575
    builder.format(move |buf, record| {
3,808✔
576
        let Ok(ts) = jiff::Timestamp::try_from(SystemTime::now()) else {
7,600✔
577
            return Err(std::io::Error::other("Failed to get timestamp"));
×
578
        };
579
        let style = buf.default_level_style(record.level());
19,000✔
580
        let elapsed = start.elapsed();
11,400✔
581
        let span = jiff::Span::new()
7,600✔
582
            .seconds(elapsed.as_secs() as i64)
7,600✔
583
            .milliseconds(elapsed.subsec_millis() as i64);
7,600✔
584
        let span = span.round(span_round).expect("Failed to round span");
22,800✔
585
        writeln!(
3,800✔
586
            buf,
3,800✔
587
            "{} {} {style}{}{style:#} [{:?}] {} - {}",
3,800✔
588
            ts.strftime("%F %T%.3f"),
11,400✔
589
            printer.span_to_string(&span),
11,400✔
590
            record.level(),
7,600✔
591
            std::thread::current().id(),
7,600✔
592
            record.target(),
7,600✔
593
            record.args()
7,600✔
594
        )
595
    });
596
    builder.init();
8✔
597
    Ok(())
4✔
598
}
599

600
#[derive(Args, Debug)]
601
pub struct GlobalArgs {
602
    #[arg(long, value_parser = parse_duration, global=true, display_order = 1000)]
603
    /// How often to log progress. Default is 10s. You can use the suffixes `s`
604
    /// for seconds, `m` for minutes, `h` for hours, and `d` for days. If no
605
    /// suffix is provided it is assumed to be in milliseconds.
606
    /// Example: `1d2h3m4s567` is parsed as 1 day + 2 hours + 3 minutes + 4
607
    /// seconds + 567 milliseconds = 93784567 milliseconds.
608
    pub log_interval: Option<Duration>,
609
}
610

611
#[derive(Subcommand, Debug)]
612
pub enum SubCommands {
613
    #[command(subcommand)]
614
    Analyze(analyze::SubCommands),
615
    #[command(subcommand)]
616
    Bench(bench::SubCommands),
617
    #[command(subcommand)]
618
    Build(build::SubCommands),
619
    #[command(subcommand)]
620
    Check(check::SubCommands),
621
    #[command(subcommand)]
622
    From(from::SubCommands),
623
    #[command(subcommand)]
624
    Perm(perm::SubCommands),
625
    #[command(subcommand)]
626
    Run(run::SubCommands),
627
    #[command(subcommand)]
628
    To(to::SubCommands),
629
    #[command(subcommand)]
630
    Transform(transform::SubCommands),
631
}
632

633
#[derive(Parser, Debug)]
634
#[command(name = "webgraph", version=build_info::version_string())]
635
/// Webgraph tools to build, convert, modify, and analyze graphs.
636
#[doc = include_str!("common_env.txt")]
637
pub struct Cli {
638
    #[command(subcommand)]
639
    pub command: SubCommands,
640
    #[clap(flatten)]
641
    pub args: GlobalArgs,
642
}
643

644
pub mod dist;
645
pub mod sccs;
646

647
pub mod analyze;
648
pub mod bench;
649
pub mod build;
650
pub mod check;
651
pub mod from;
652
pub mod perm;
653
pub mod run;
654
pub mod to;
655
pub mod transform;
656

657
/// The entry point of the command-line interface.
658
pub fn cli_main<I, T>(args: I) -> Result<()>
12✔
659
where
660
    I: IntoIterator<Item = T>,
661
    T: Into<std::ffi::OsString> + Clone,
662
{
663
    let start = std::time::Instant::now();
24✔
664
    let cli = Cli::parse_from(args);
36✔
665
    match cli.command {
12✔
666
        SubCommands::Analyze(args) => {
×
667
            analyze::main(cli.args, args)?;
×
668
        }
669
        SubCommands::Bench(args) => {
×
670
            bench::main(cli.args, args)?;
×
671
        }
672
        SubCommands::Build(args) => {
7✔
673
            build::main(cli.args, args, Cli::command())?;
28✔
674
        }
675
        SubCommands::Check(args) => {
×
676
            check::main(cli.args, args)?;
×
677
        }
678
        SubCommands::From(args) => {
×
679
            from::main(cli.args, args)?;
×
680
        }
681
        SubCommands::Perm(args) => {
2✔
682
            perm::main(cli.args, args)?;
6✔
683
        }
684
        SubCommands::Run(args) => {
1✔
685
            run::main(cli.args, args)?;
3✔
686
        }
687
        SubCommands::To(args) => {
1✔
688
            to::main(cli.args, args)?;
3✔
689
        }
690
        SubCommands::Transform(args) => {
1✔
691
            transform::main(cli.args, args)?;
3✔
692
        }
693
    }
694

695
    log::info!(
12✔
696
        "The command took {}",
12✔
697
        pretty_print_elapsed(start.elapsed().as_secs_f64())
36✔
698
    );
699

700
    Ok(())
12✔
701
}
702

703
/// Pretty prints seconds in a humanly readable format.
704
fn pretty_print_elapsed(elapsed: f64) -> String {
48✔
705
    let mut result = String::new();
96✔
706
    let mut elapsed_seconds = elapsed as u64;
96✔
707
    let weeks = elapsed_seconds / (60 * 60 * 24 * 7);
96✔
708
    elapsed_seconds %= 60 * 60 * 24 * 7;
48✔
709
    let days = elapsed_seconds / (60 * 60 * 24);
96✔
710
    elapsed_seconds %= 60 * 60 * 24;
48✔
711
    let hours = elapsed_seconds / (60 * 60);
96✔
712
    elapsed_seconds %= 60 * 60;
48✔
713
    let minutes = elapsed_seconds / 60;
96✔
714
    //elapsed_seconds %= 60;
715

716
    match weeks {
48✔
717
        0 => {}
48✔
718
        1 => result.push_str("1 week "),
×
719
        _ => result.push_str(&format!("{} weeks ", weeks)),
×
720
    }
721
    match days {
48✔
722
        0 => {}
48✔
723
        1 => result.push_str("1 day "),
×
724
        _ => result.push_str(&format!("{} days ", days)),
×
725
    }
726
    match hours {
48✔
727
        0 => {}
48✔
728
        1 => result.push_str("1 hour "),
×
729
        _ => result.push_str(&format!("{} hours ", hours)),
×
730
    }
731
    match minutes {
48✔
732
        0 => {}
44✔
733
        1 => result.push_str("1 minute "),
12✔
UNCOV
734
        _ => result.push_str(&format!("{} minutes ", minutes)),
×
735
    }
736

737
    result.push_str(&format!("{:.3} seconds ({}s)", elapsed % 60.0, elapsed));
192✔
738
    result
48✔
739
}
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