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

facet-rs / facet / 14418933622

12 Apr 2025 10:54AM UTC coverage: 31.872% (-0.7%) from 32.576%
14418933622

Pull #180

github

web-flow
Merge d333829be into 4b37a5a95
Pull Request #180: Impl `Facet` for `Arc<T>`

18 of 177 new or added lines in 7 files covered. (10.17%)

45 existing lines in 1 file now uncovered.

2007 of 6297 relevant lines covered (31.87%)

13.52 hits per line

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

0.0
/facet-codegen/src/main.rs
1
use std::io::Write;
2
use std::path::Path;
3
use std::process;
4

5
mod gen_tuples_impls;
6
mod readmes;
7

8
use facet_ansi::Stylize as _;
9
use log::{error, info, warn};
10
use similar::{ChangeTag, TextDiff};
11

12
fn main() {
×
13
    facet_testhelpers::setup();
×
14

15
    let opts = Options {
16
        check: std::env::args().any(|arg| arg == "--check"),
×
17
    };
18
    let mut has_diffs = false;
×
19

20
    // Check if current directory has a Cargo.toml with [workspace]
21
    let cargo_toml_path = std::env::current_dir().unwrap().join("Cargo.toml");
×
22
    let cargo_toml_content =
×
23
        fs_err::read_to_string(cargo_toml_path).expect("Failed to read Cargo.toml");
×
24
    if !cargo_toml_content.contains("[workspace]") {
×
25
        error!("🚫 {}", "Cargo.toml does not contain [workspace] (you must run codegen from the workspace root)".red());
×
26
        panic!();
×
27
    }
28

29
    // Run all three code generation tasks in parallel
30
    let opts_clone1 = opts.clone();
×
31
    let tuple_impls_result = std::thread::spawn(move || {
×
32
        let mut local_has_diffs = false;
×
33
        generate_tuple_impls(&mut local_has_diffs, opts_clone1);
×
34
        local_has_diffs
×
35
    });
36

37
    let opts_clone2 = opts.clone();
×
38
    let readme_had_diffs = std::thread::spawn(move || readmes::generate_readme_files(opts_clone2));
×
39

40
    let opts_clone3 = opts.clone();
×
41
    let sample_code_result = std::thread::spawn(move || {
×
42
        let mut local_has_diffs = false;
×
43
        copy_cargo_expand_output(&mut local_has_diffs, &opts_clone3);
×
44
        local_has_diffs
×
45
    });
46

47
    // Collect results and update has_diffs
48
    has_diffs |= tuple_impls_result
×
49
        .join()
×
50
        .expect("tuple_impls thread panicked");
×
51
    has_diffs |= readme_had_diffs
×
52
        .join()
×
53
        .expect("readme_files thread panicked");
×
54
    has_diffs |= sample_code_result
×
55
        .join()
×
56
        .expect("sample_code thread panicked");
×
57

58
    if opts.check && has_diffs {
×
59
        // Print a big banner with error message about generated files
60
        error!("┌────────────────────────────────────────────────────────────────────────────┐");
×
61
        error!("│                                                                            │");
×
62
        error!("│  GENERATED FILES HAVE CHANGED - RUN `just codegen` TO UPDATE THEM          │");
×
63
        error!("│                                                                            │");
×
64
        error!("│  For README.md files:                                                      │");
×
65
        error!("│                                                                            │");
×
66
        error!("│  • Don't edit README.md directly - edit the README.md.in template instead  │");
×
67
        error!("│  • Then run `just codegen` to regenerate the README.md files               │");
×
68
        error!("│  • A pre-commit hook is set up by cargo-husky to do just that              │");
×
69
        error!("│                                                                            │");
×
70
        error!("│  See CONTRIBUTING.md                                                       │");
×
71
        error!("│                                                                            │");
×
72
        error!("└────────────────────────────────────────────────────────────────────────────┘");
×
73
        process::exit(1);
×
74
    }
75
}
76

77
fn copy_cargo_expand_output(has_diffs: &mut bool, opts: &Options) {
×
78
    let workspace_dir = std::env::current_dir().unwrap();
×
79
    let sample_dir = workspace_dir.join("sample");
×
80

81
    // Run cargo expand command and measure execution time
82
    let start_time = std::time::Instant::now();
×
83

84
    // Command 1: cargo rustc for expansion
85
    let cargo_expand_output = std::process::Command::new("cargo")
×
86
        .env("RUSTC_BOOTSTRAP", "1") // Necessary for -Z flags
87
        .current_dir(&sample_dir) // Set working directory instead of changing it
×
88
        .arg("rustc")
89
        .arg("--target-dir")
90
        .arg("/tmp/facet-codegen-expand") // Use a temporary, less intrusive target dir
91
        .arg("--lib") // Expand the library crate in the current directory
92
        .arg("--") // Separator for rustc flags
93
        .arg("-Zunpretty=expanded") // The flag to expand macros
94
        .output() // Execute and capture output
95
        .expect("Failed to execute cargo rustc for expansion");
96

97
    // Check if cargo rustc succeeded
98
    if !cargo_expand_output.status.success() {
×
99
        error!(
×
100
            "🚫 {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
×
101
            "cargo rustc expansion failed".red(),
×
102
            String::from_utf8_lossy(&cargo_expand_output.stderr).trim(),
×
103
            String::from_utf8_lossy(&cargo_expand_output.stdout).trim()
×
104
        );
105
        std::process::exit(1);
×
106
    }
107

108
    // Prepare the code for rustfmt: prepend the necessary lines
109
    let expanded_code = String::from_utf8(cargo_expand_output.stdout)
×
110
        .expect("Failed to convert cargo expand output to UTF-8 string");
111

112
    // Command 2: rustfmt to format the expanded code
113
    let mut rustfmt_cmd = std::process::Command::new("rustfmt")
×
114
        .arg("--edition")
115
        .arg("2024")
116
        .arg("--emit")
117
        .arg("stdout")
118
        .stdin(std::process::Stdio::piped()) // Prepare to pipe stdin
×
119
        .stdout(std::process::Stdio::piped()) // Capture stdout
×
120
        .stderr(std::process::Stdio::piped()) // Capture stderr
×
121
        .spawn()
122
        .expect("Failed to spawn rustfmt");
123

124
    // Write the combined code to rustfmt's stdin in a separate scope
125
    // to ensure stdin is closed, signaling EOF to rustfmt.
126
    {
127
        let mut stdin = rustfmt_cmd
×
128
            .stdin
×
129
            .take()
130
            .expect("Failed to open rustfmt stdin");
131
        stdin
×
132
            .write_all(expanded_code.as_bytes())
×
133
            .expect("Failed to write to rustfmt stdin");
134
    } // stdin is closed here
×
135

136
    // Wait for rustfmt to finish and collect its output
137
    let output = rustfmt_cmd
×
138
        .wait_with_output()
139
        .expect("Failed to wait for rustfmt");
140

141
    // Check if rustfmt succeeded (using the final 'output' variable)
142
    // Note: The original code only checked the final status, which might hide
143
    // the cargo expand error if rustfmt succeeds. We now check both stages.
144
    if !output.status.success() {
×
145
        error!(
×
146
            "🚫 {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
×
147
            "rustfmt failed".red(),
×
148
            String::from_utf8_lossy(&output.stderr).trim(),
×
149
            String::from_utf8_lossy(&output.stdout).trim()
×
150
        );
151
        // We still need to check the final status for the rest of the function
152
        // but the process might have already exited if cargo expand failed.
153
        // If rustfmt itself fails, exit here.
154
        std::process::exit(1);
×
155
    }
156
    let execution_time = start_time.elapsed();
×
157

158
    if !output.status.success() {
×
159
        error!("🚫 {}", "Cargo expand command failed".red());
×
160
        std::process::exit(1);
×
161
    }
162

163
    let expanded_code =
×
164
        String::from_utf8(output.stdout).expect("Failed to convert output to string");
×
165

166
    // First collect doc comments, then filter out lines we don't want
167
    let doc_comments = expanded_code
×
168
        .lines()
169
        .filter(|line| line.trim_start().starts_with("//!"))
×
170
        .collect::<Vec<_>>()
171
        .join("\n");
172

173
    let expanded_code = expanded_code
×
174
        .lines()
175
        .filter(|line| {
×
176
            let trimmed = line.trim_start();
×
177
            !trimmed.starts_with("#![")
×
178
                && !trimmed.starts_with("#[facet(")
×
179
                && !trimmed.starts_with("#[macro_use]")
×
180
                && !trimmed.starts_with("//!")
×
181
        })
182
        .collect::<Vec<_>>()
183
        .join("\n");
184

185
    // Ensure a trailing newline for consistency
186
    let expanded_code = if expanded_code.is_empty() {
×
187
        String::new()
×
188
    } else {
189
        format!("{}\n", expanded_code)
×
190
    };
191

192
    // Replace any ::facet:: references with crate::
UNCOV
193
    let expanded_code = expanded_code.replace("::facet::", "crate::");
×
194
    let expanded_code = expanded_code.replace("use facet::", "use crate::");
×
195

UNCOV
196
    let expanded_code = format!("{}\n#![allow(warnings)]\n{}", doc_comments, expanded_code);
×
197

UNCOV
198
    let expanded_code = expanded_code.replace(
×
199
        "::impls::_core::marker::PhantomData",
200
        "::core::marker::PhantomData",
201
    );
202

203
    // Write the expanded code to the target file
UNCOV
204
    let target_path = workspace_dir
×
205
        .join("facet")
206
        .join("src")
207
        .join("sample_generated_code.rs");
208

209
    let was_different = write_if_different(&target_path, expanded_code.into_bytes(), opts.check);
×
UNCOV
210
    *has_diffs |= was_different;
×
211

212
    if opts.check {
×
213
        info!(
×
214
            "✅ Checked {} (took {:?})",
×
UNCOV
215
            "sample_generated_code.rs".blue().green(),
×
216
            execution_time
217
        );
218
    } else if was_different {
×
219
        info!(
×
220
            "🔧 Generated {} (took {:?})",
×
UNCOV
221
            "sample_generated_code.rs".blue().green(),
×
222
            execution_time
223
        );
224
    } else {
225
        info!(
×
226
            "✅ No changes to {} (took {:?})",
×
UNCOV
227
            "sample_generated_code.rs".blue().green(),
×
228
            execution_time
229
        );
230
    }
231
}
232

233
#[derive(Debug, Clone)]
234
struct Options {
235
    check: bool,
236
}
237

238
fn check_diff(path: &Path, new_content: &[u8]) -> bool {
×
239
    if !path.exists() {
×
240
        warn!(
×
241
            "📁 {}: {}",
×
242
            path.display(),
×
UNCOV
243
            "would create new file".yellow()
×
244
        );
UNCOV
245
        return true;
×
246
    }
247

248
    let old_content = fs_err::read(path).unwrap();
×
249
    if old_content != new_content {
×
250
        let old_str = String::from_utf8_lossy(&old_content);
×
UNCOV
251
        let new_str = String::from_utf8_lossy(new_content);
×
252

253
        let diff = TextDiff::from_lines(&old_str, &new_str);
×
UNCOV
254
        info!("📝 {}", format!("Diff for {}:", path.display()).blue());
×
255

256
        // Track consecutive equal lines
257
        let mut equal_count = 0;
×
UNCOV
258
        let mut last_tag = None;
×
259

260
        for change in diff.iter_all_changes() {
×
UNCOV
261
            let tag = change.tag();
×
262

263
            // If we're switching from Equal to another tag, and we have >=4 equal lines, show the count
264
            if last_tag == Some(ChangeTag::Equal) && tag != ChangeTag::Equal && equal_count > 3 {
×
265
                info!(" {} lines omitted.", equal_count - 1);
×
UNCOV
266
                equal_count = 0;
×
267
            }
268

UNCOV
269
            match tag {
×
270
                ChangeTag::Equal => {
UNCOV
271
                    if equal_count == 0 {
×
272
                        // Always show the first equal line
273
                        info!(" {}", change);
×
UNCOV
274
                    } else if equal_count < 3 {
×
275
                        // Show the 2nd and 3rd equal lines
UNCOV
276
                        info!(" {}", change);
×
277
                    }
UNCOV
278
                    equal_count += 1;
×
279
                }
280
                ChangeTag::Delete => {
281
                    equal_count = 0;
×
UNCOV
282
                    info!("-{}", change.red());
×
283
                }
284
                ChangeTag::Insert => {
285
                    equal_count = 0;
×
UNCOV
286
                    info!("+{}", change.green());
×
287
                }
288
            }
289

UNCOV
290
            last_tag = Some(tag);
×
291
        }
292

293
        // Handle case where diff ends with equal lines
294
        if last_tag == Some(ChangeTag::Equal) && equal_count > 3 {
×
UNCOV
295
            info!(" {} lines omitted.", equal_count - 1);
×
296
        }
297

UNCOV
298
        return true;
×
299
    }
UNCOV
300
    false
×
301
}
302

303
fn write_if_different(path: &Path, content: Vec<u8>, check_mode: bool) -> bool {
×
304
    let is_different = check_diff(path, &content);
×
305
    if check_mode {
×
306
        is_different
×
307
    } else if is_different {
×
308
        info!("Overwriting {} (had changes)", path.display().blue());
×
309
        fs_err::write(path, content).expect("Failed to write file");
×
UNCOV
310
        true
×
311
    } else {
UNCOV
312
        false
×
313
    }
314
}
315

UNCOV
316
fn generate_tuple_impls(has_diffs: &mut bool, opts: Options) {
×
317
    // Start timer to measure execution time
UNCOV
318
    let start_time = std::time::Instant::now();
×
319

320
    // Define the base path and template path
UNCOV
321
    let base_path = Path::new("facet-core/src/_trait/impls/tuples_impls.rs");
×
322

UNCOV
323
    let output = gen_tuples_impls::generate_tuples_impls();
×
324

325
    // Format the generated code using rustfmt
UNCOV
326
    let mut fmt = std::process::Command::new("rustfmt")
×
327
        .arg("--edition")
328
        .arg("2024")
329
        .stdin(std::process::Stdio::piped())
×
UNCOV
330
        .stdout(std::process::Stdio::piped())
×
331
        .spawn()
332
        .expect("Failed to spawn rustfmt");
333

334
    // Write to rustfmt's stdin
UNCOV
335
    fmt.stdin
×
336
        .take()
337
        .expect("Failed to get stdin")
UNCOV
338
        .write_all(output.as_bytes())
×
339
        .expect("Failed to write to rustfmt stdin");
340

341
    // Get formatted output
342
    let formatted_output = fmt.wait_with_output().expect("Failed to wait for rustfmt");
×
UNCOV
343
    if !formatted_output.status.success() {
×
344
        // Save the problematic output for inspection
345
        let _ = std::fs::write("/tmp/output.rs", &output);
×
346
        error!(
×
347
            "🚫 {} {}",
×
348
            "rustfmt failed to format the code.".red(),
×
UNCOV
349
            "The unformatted output has been saved to /tmp/output.rs for inspection.".yellow(),
×
350
        );
351

352
        error!(
×
353
            "🚫 {}",
×
UNCOV
354
            format!("rustfmt failed with exit code: {}", formatted_output.status).red()
×
355
        );
UNCOV
356
        std::process::exit(1);
×
357
    }
358

359
    let was_different = write_if_different(base_path, formatted_output.stdout, opts.check);
×
UNCOV
360
    *has_diffs |= was_different;
×
361

362
    // Calculate execution time
UNCOV
363
    let execution_time = start_time.elapsed();
×
364

365
    // Print success message with execution time
366
    if opts.check {
×
367
        info!(
×
368
            "✅ Checked {} (took {:?})",
×
UNCOV
369
            "tuple implementations".blue().green(),
×
370
            execution_time
371
        );
372
    } else if was_different {
×
373
        info!(
×
374
            "🔧 Generated {} (took {:?})",
×
UNCOV
375
            "tuple implementations".blue().green(),
×
376
            execution_time
377
        );
378
    } else {
379
        info!(
×
380
            "✅ No changes to {} (took {:?})",
×
UNCOV
381
            "tuple implementations".blue().green(),
×
382
            execution_time
383
        );
384
    }
385
}
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