tenseleyflow/bencch / 76c455e

Browse files

Probe compiler adapters

Authored by espadonne
SHA
76c455e2562aa6f40ef90d237361ff071332247c
Parents
af1b2ba
Tree
533ea93

1 changed file

StatusFile+-
M bench/src/lib.rs 300 19
bench/src/lib.rsmodified
@@ -443,21 +443,22 @@ fn named_compiler_status_value(
443443
     tools: &ToolchainConfig,
444444
     capture_root: Option<&PathBuf>,
445445
 ) -> String {
446
-    match named {
447
-        NamedCompiler::Armfortas => match &tools.armfortas {
448
-            ArmfortasCliAdapter::Linked => capture_root
449
-                .map(|root| format!("linked via Cargo to {}", display_path(root)))
450
-                .unwrap_or_else(|| {
451
-                    "linked adapter requested but unavailable in this build".to_string()
452
-                }),
453
-            ArmfortasCliAdapter::External(binary) => tool_probe_status(binary, false),
454
-        },
455
-        _ => tool_probe_status(
456
-            &tools
457
-                .named_compiler_binary(named)
458
-                .unwrap_or_else(|| named.as_str().to_string()),
459
-            false,
446
+    let probe = named_compiler_probe(named, tools, capture_root);
447
+    match probe.resolved_path {
448
+        Some(path) => format!(
449
+            "configured={} resolved={}",
450
+            probe.configured,
451
+            path.display()
460452
         ),
453
+        None => {
454
+            if probe.status == "linked" {
455
+                probe
456
+                    .detail
457
+                    .unwrap_or_else(|| "linked via Cargo".to_string())
458
+            } else {
459
+                format!("configured={} resolved=missing", probe.configured)
460
+            }
461
+        }
461462
     }
462463
 }
463464
 
@@ -469,6 +470,7 @@ fn append_named_compiler_fields(
469470
 ) {
470471
     let prefix = format!("named_compiler.{}", named.as_str());
471472
     let capabilities = compiler_capabilities(&CompilerSpec::Named(named), tools);
473
+    let probe = named_compiler_probe(named, tools, capture_root);
472474
     if named == NamedCompiler::Armfortas {
473475
         let armfortas = tools.armfortas_adapters();
474476
         fields.push((
@@ -505,6 +507,23 @@ fn append_named_compiler_fields(
505507
         format!("{}.unavailable_artifacts", prefix),
506508
         capability_unavailable_summary(&capabilities),
507509
     ));
510
+    fields.push((format!("{}.probe_status", prefix), probe.status.clone()));
511
+    fields.push((
512
+        format!("{}.probe_resolved_path", prefix),
513
+        probe
514
+            .resolved_path
515
+            .as_ref()
516
+            .map(|path| display_path(path))
517
+            .unwrap_or_else(|| "none".to_string()),
518
+    ));
519
+    fields.push((
520
+        format!("{}.probe_banner", prefix),
521
+        probe.banner.clone().unwrap_or_else(|| "none".to_string()),
522
+    ));
523
+    fields.push((
524
+        format!("{}.probe_detail", prefix),
525
+        probe.detail.clone().unwrap_or_else(|| "none".to_string()),
526
+    ));
508527
 }
509528
 
510529
 fn compiler_capability_backend(spec: &CompilerSpec, tools: &ToolchainConfig) -> (String, String) {
@@ -3446,6 +3465,26 @@ fn render_doctor_capabilities_json(capabilities: &CompilerCapabilities) -> Strin
34463465
     )
34473466
 }
34483467
 
3468
+fn render_tool_probe_json(probe: &ToolProbe) -> String {
3469
+    format!(
3470
+        "{{\"configured\":\"{}\",\"status\":\"{}\",\"resolved_path\":{},\"banner\":{},\"detail\":{}}}",
3471
+        json_escape(&probe.configured),
3472
+        json_escape(&probe.status),
3473
+        match probe.resolved_path.as_ref() {
3474
+            Some(path) => format!("\"{}\"", json_escape(&display_path(path))),
3475
+            None => "null".to_string(),
3476
+        },
3477
+        match probe.banner.as_ref() {
3478
+            Some(banner) => format!("\"{}\"", json_escape(banner)),
3479
+            None => "null".to_string(),
3480
+        },
3481
+        match probe.detail.as_ref() {
3482
+            Some(detail) => format!("\"{}\"", json_escape(detail)),
3483
+            None => "null".to_string(),
3484
+        },
3485
+    )
3486
+}
3487
+
34493488
 fn json_string_vec_map(map: &BTreeMap<String, Vec<String>>) -> String {
34503489
     let mut rendered = String::from("{");
34513490
     for (index, (key, values)) in map.iter().enumerate() {
@@ -3467,6 +3506,7 @@ fn render_named_compiler_entry_json(
34673506
     capture_root: Option<&PathBuf>,
34683507
 ) -> String {
34693508
     let capabilities = compiler_capabilities(&CompilerSpec::Named(named), tools);
3509
+    let probe = named_compiler_probe(named, tools, capture_root);
34703510
     let mut fields = vec![
34713511
         format!(
34723512
             "\"accepted_names\": {}",
@@ -3480,6 +3520,7 @@ fn render_named_compiler_entry_json(
34803520
             "\"capabilities\": {}",
34813521
             render_doctor_capabilities_json(&capabilities)
34823522
         ),
3523
+        format!("\"probe\": {}", render_tool_probe_json(&probe)),
34833524
     ];
34843525
     if named == NamedCompiler::Armfortas {
34853526
         let armfortas = tools.armfortas_adapters();
@@ -3780,8 +3821,17 @@ fn doctor_markdown_cell(value: &str) -> String {
37803821
     value.replace('|', "\\|").replace('\n', "<br>")
37813822
 }
37823823
 
3783
-fn tool_probe_status(configured: &str, already_resolved_path: bool) -> String {
3784
-    let resolved = if already_resolved_path {
3824
+#[derive(Debug, Clone, PartialEq, Eq)]
3825
+struct ToolProbe {
3826
+    configured: String,
3827
+    resolved_path: Option<PathBuf>,
3828
+    status: String,
3829
+    banner: Option<String>,
3830
+    detail: Option<String>,
3831
+}
3832
+
3833
+fn tool_probe(configured: &str, already_resolved_path: bool) -> ToolProbe {
3834
+    let resolved_path = if already_resolved_path {
37853835
         let path = PathBuf::from(configured);
37863836
         if path.exists() {
37873837
             Some(path)
@@ -3792,7 +3842,126 @@ fn tool_probe_status(configured: &str, already_resolved_path: bool) -> String {
37923842
         resolve_tool_path(configured)
37933843
     };
37943844
 
3795
-    match resolved {
3845
+    match resolved_path {
3846
+        Some(path) => match probe_tool_banner(&path) {
3847
+            Ok((arg, banner)) => ToolProbe {
3848
+                configured: configured.to_string(),
3849
+                resolved_path: Some(path),
3850
+                status: "invokable".into(),
3851
+                banner: Some(banner),
3852
+                detail: Some(format!("probe succeeded with {}", arg)),
3853
+            },
3854
+            Err(detail) => ToolProbe {
3855
+                configured: configured.to_string(),
3856
+                resolved_path: Some(path),
3857
+                status: "resolved".into(),
3858
+                banner: None,
3859
+                detail: Some(detail),
3860
+            },
3861
+        },
3862
+        None => ToolProbe {
3863
+            configured: configured.to_string(),
3864
+            resolved_path: None,
3865
+            status: "missing".into(),
3866
+            banner: None,
3867
+            detail: Some("binary not found on disk or PATH".into()),
3868
+        },
3869
+    }
3870
+}
3871
+
3872
+fn linked_tool_probe(capture_root: Option<&PathBuf>) -> ToolProbe {
3873
+    match capture_root {
3874
+        Some(root) => ToolProbe {
3875
+            configured: "linked".into(),
3876
+            resolved_path: Some(root.clone()),
3877
+            status: "linked".into(),
3878
+            banner: None,
3879
+            detail: Some(format!("linked via Cargo to {}", display_path(root))),
3880
+        },
3881
+        None => ToolProbe {
3882
+            configured: "linked".into(),
3883
+            resolved_path: None,
3884
+            status: "unavailable".into(),
3885
+            banner: None,
3886
+            detail: Some("linked adapter requested but unavailable in this build".into()),
3887
+        },
3888
+    }
3889
+}
3890
+
3891
+fn named_compiler_probe(
3892
+    named: NamedCompiler,
3893
+    tools: &ToolchainConfig,
3894
+    capture_root: Option<&PathBuf>,
3895
+) -> ToolProbe {
3896
+    match named {
3897
+        NamedCompiler::Armfortas => match &tools.armfortas {
3898
+            ArmfortasCliAdapter::Linked => linked_tool_probe(capture_root),
3899
+            ArmfortasCliAdapter::External(binary) => tool_probe(binary, false),
3900
+        },
3901
+        _ => tool_probe(
3902
+            &tools
3903
+                .named_compiler_binary(named)
3904
+                .unwrap_or_else(|| named.as_str().to_string()),
3905
+            false,
3906
+        ),
3907
+    }
3908
+}
3909
+
3910
+fn compiler_spec_probe(
3911
+    spec: &CompilerSpec,
3912
+    tools: &ToolchainConfig,
3913
+    capture_root: Option<&PathBuf>,
3914
+) -> ToolProbe {
3915
+    match spec {
3916
+        CompilerSpec::Named(named) => named_compiler_probe(*named, tools, capture_root),
3917
+        CompilerSpec::Binary(path) => tool_probe(&path.display().to_string(), true),
3918
+    }
3919
+}
3920
+
3921
+fn probe_tool_banner(path: &Path) -> Result<(String, String), String> {
3922
+    let mut last_detail = None;
3923
+    for arg in ["--version", "-V", "-v"] {
3924
+        match Command::new(path).arg(arg).output() {
3925
+            Ok(output) => {
3926
+                let stdout = String::from_utf8_lossy(&output.stdout);
3927
+                let stderr = String::from_utf8_lossy(&output.stderr);
3928
+                let combined = stdout
3929
+                    .lines()
3930
+                    .chain(stderr.lines())
3931
+                    .map(str::trim)
3932
+                    .find(|line| !line.is_empty())
3933
+                    .map(|line| compact_probe_banner(line));
3934
+                if let Some(line) = combined {
3935
+                    return Ok((arg.to_string(), line));
3936
+                }
3937
+                last_detail = Some(format!(
3938
+                    "{} returned no banner output (exit={})",
3939
+                    arg,
3940
+                    output.status.code().unwrap_or(-1)
3941
+                ));
3942
+            }
3943
+            Err(err) => {
3944
+                last_detail = Some(format!("{} failed: {}", arg, err));
3945
+            }
3946
+        }
3947
+    }
3948
+
3949
+    Err(last_detail.unwrap_or_else(|| "probe failed".to_string()))
3950
+}
3951
+
3952
+fn compact_probe_banner(line: &str) -> String {
3953
+    let compact = line.split_whitespace().collect::<Vec<_>>().join(" ");
3954
+    let chars = compact.chars().collect::<Vec<_>>();
3955
+    if chars.len() > 120 {
3956
+        chars.into_iter().take(117).collect::<String>() + "..."
3957
+    } else {
3958
+        compact
3959
+    }
3960
+}
3961
+
3962
+fn tool_probe_status(configured: &str, already_resolved_path: bool) -> String {
3963
+    let probe = tool_probe(configured, already_resolved_path);
3964
+    match probe.resolved_path {
37963965
         Some(path) => format!("configured={} resolved={}", configured, path.display()),
37973966
         None => format!("configured={} resolved=missing", configured),
37983967
     }
@@ -4885,7 +5054,21 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String>
48855054
     }
48865055
 
48875056
     if let Some(generic) = &case.generic_introspect {
5057
+        let capture_root = tools.armfortas_adapters().capture_root();
5058
+        let probe = compiler_spec_probe(&generic.compiler, tools, capture_root.as_ref());
48885059
         lines.push(format!("compiler: {}", generic.compiler.display_name()));
5060
+        lines.push(format!("compiler_probe_status: {}", probe.status));
5061
+        lines.push(format!(
5062
+            "compiler_probe_resolved_path: {}",
5063
+            probe
5064
+                .resolved_path
5065
+                .as_ref()
5066
+                .map(|path| display_path(path))
5067
+                .unwrap_or_else(|| "none".to_string())
5068
+        ));
5069
+        if let Some(banner) = &probe.banner {
5070
+            lines.push(format!("compiler_probe_banner: {}", banner));
5071
+        }
48895072
         lines.push(format!(
48905073
             "artifacts: {}",
48915074
             format_artifact_name_list(
@@ -4915,11 +5098,38 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String>
49155098
     }
49165099
 
49175100
     if let Some(generic) = &case.generic_compare {
5101
+        let capture_root = tools.armfortas_adapters().capture_root();
5102
+        let left_probe = compiler_spec_probe(&generic.left, tools, capture_root.as_ref());
5103
+        let right_probe = compiler_spec_probe(&generic.right, tools, capture_root.as_ref());
49185104
         lines.push(format!(
49195105
             "compare: {} vs {}",
49205106
             generic.left.display_name(),
49215107
             generic.right.display_name()
49225108
         ));
5109
+        lines.push(format!("left_probe_status: {}", left_probe.status));
5110
+        lines.push(format!(
5111
+            "left_probe_resolved_path: {}",
5112
+            left_probe
5113
+                .resolved_path
5114
+                .as_ref()
5115
+                .map(|path| display_path(path))
5116
+                .unwrap_or_else(|| "none".to_string())
5117
+        ));
5118
+        if let Some(banner) = &left_probe.banner {
5119
+            lines.push(format!("left_probe_banner: {}", banner));
5120
+        }
5121
+        lines.push(format!("right_probe_status: {}", right_probe.status));
5122
+        lines.push(format!(
5123
+            "right_probe_resolved_path: {}",
5124
+            right_probe
5125
+                .resolved_path
5126
+                .as_ref()
5127
+                .map(|path| display_path(path))
5128
+                .unwrap_or_else(|| "none".to_string())
5129
+        ));
5130
+        if let Some(banner) = &right_probe.banner {
5131
+            lines.push(format!("right_probe_banner: {}", banner));
5132
+        }
49235133
         lines.push(format!(
49245134
             "artifacts: {}",
49255135
             format_artifact_name_list(
@@ -10992,6 +11202,14 @@ mod tests {
1099211202
         fs::set_permissions(path, perms).unwrap();
1099311203
     }
1099411204
 
11205
+    #[cfg(unix)]
11206
+    fn write_probe_script(path: &Path, banner: &str) {
11207
+        fs::write(path, format!("#!/bin/sh\nprintf '%s\\n' {:?}\n", banner)).unwrap();
11208
+        let mut perms = fs::metadata(path).unwrap().permissions();
11209
+        perms.set_mode(0o755);
11210
+        fs::set_permissions(path, perms).unwrap();
11211
+    }
11212
+
1099511213
     #[cfg(unix)]
1099611214
     fn command_is_available(name: &str) -> bool {
1099711215
         Command::new("which")
@@ -14133,8 +14351,8 @@ end
1413314351
         fs::create_dir_all(&root).unwrap();
1413414352
         let armfortas_bin = root.join("armfortas");
1413514353
         let gfortran_bin = root.join("gfortran");
14136
-        fs::write(&armfortas_bin, "").unwrap();
14137
-        fs::write(&gfortran_bin, "").unwrap();
14354
+        write_probe_script(&armfortas_bin, "armfortas dev build");
14355
+        write_probe_script(&gfortran_bin, "GNU Fortran 99.1");
1413814356
 
1413914357
         let config = DoctorConfig {
1414014358
             tools: ToolchainConfig {
@@ -14189,6 +14407,10 @@ end
1418914407
         assert!(rendered.contains("named_compiler.lfortran.candidate_binaries: lfortran"));
1419014408
         assert!(rendered.contains("named_compiler.ifx.accepted_names: ifx"));
1419114409
         assert!(rendered.contains("named_compiler.nvfortran.accepted_names: nvfortran, pgfortran"));
14410
+        assert!(rendered.contains("named_compiler.armfortas.probe_status: invokable"));
14411
+        assert!(rendered.contains("named_compiler.armfortas.probe_banner: armfortas dev build"));
14412
+        assert!(rendered.contains("named_compiler.gfortran.probe_status: invokable"));
14413
+        assert!(rendered.contains("named_compiler.gfortran.probe_banner: GNU Fortran 99.1"));
1419214414
         assert!(rendered.contains(
1419314415
             "explicit_compiler_path: any filesystem path passed to compare/introspect uses the generic external-driver adapter"
1419414416
         ));
@@ -14214,12 +14436,34 @@ end
1421414436
         assert!(rendered_json.contains("\"tools\": {"));
1421514437
         assert!(rendered_json.contains("\"lfortran\": {"));
1421614438
         assert!(rendered_json.contains("\"named_compiler.armfortas.adapter_extras\""));
14439
+        assert!(rendered_json.contains("\"probe\": {"));
1421714440
         assert!(rendered_markdown.contains("# bencch doctor report"));
1421814441
         assert!(rendered_markdown.contains("| `named_compiler.armfortas` |"));
1421914442
 
1422014443
         let _ = fs::remove_dir_all(&root);
1422114444
     }
1422214445
 
14446
+    #[cfg(unix)]
14447
+    #[test]
14448
+    fn tool_probe_reads_banner_from_executable() {
14449
+        let root = std::env::temp_dir().join("bencch_tool_probe_banner");
14450
+        let _ = fs::remove_dir_all(&root);
14451
+        fs::create_dir_all(&root).unwrap();
14452
+        let probe_bin = root.join("fakefortran");
14453
+        write_probe_script(&probe_bin, "Fake Fortran 1.2.3");
14454
+
14455
+        let probe = tool_probe(&probe_bin.display().to_string(), true);
14456
+        assert_eq!(probe.status, "invokable");
14457
+        assert_eq!(probe.banner.as_deref(), Some("Fake Fortran 1.2.3"));
14458
+        assert!(probe
14459
+            .detail
14460
+            .as_deref()
14461
+            .unwrap_or_default()
14462
+            .contains("--version"));
14463
+
14464
+        let _ = fs::remove_dir_all(&root);
14465
+    }
14466
+
1422314467
     #[test]
1422414468
     fn case_discovery_lines_report_capability_block_for_generic_introspect() {
1422514469
         let case = CaseSpec {
@@ -14280,6 +14524,43 @@ end
1428014524
         ));
1428114525
     }
1428214526
 
14527
+    #[cfg(unix)]
14528
+    #[test]
14529
+    fn case_discovery_lines_include_compiler_probe_banner() {
14530
+        let root = std::env::temp_dir().join("bencch_case_discovery_probe");
14531
+        let _ = fs::remove_dir_all(&root);
14532
+        fs::create_dir_all(&root).unwrap();
14533
+        let compiler = root.join("probe-compiler");
14534
+        write_probe_script(&compiler, "Probe Compiler 7.4");
14535
+
14536
+        let case = CaseSpec {
14537
+            name: "probe".into(),
14538
+            source: PathBuf::from("demo.f90"),
14539
+            graph_files: Vec::new(),
14540
+            requested: BTreeSet::new(),
14541
+            generic_introspect: Some(GenericIntrospectCase {
14542
+                compiler: CompilerSpec::Binary(compiler.clone()),
14543
+                artifacts: BTreeSet::from([ArtifactKey::Asm]),
14544
+            }),
14545
+            generic_compare: None,
14546
+            opt_levels: vec![OptLevel::O0],
14547
+            repeat_count: 2,
14548
+            reference_compilers: Vec::new(),
14549
+            consistency_checks: Vec::new(),
14550
+            expectations: Vec::new(),
14551
+            status_rules: Vec::new(),
14552
+            capability_policy: None,
14553
+        };
14554
+
14555
+        let lines = case_discovery_lines(&case, &ToolchainConfig::from_env());
14556
+        assert!(lines.contains(&"compiler_probe_status: invokable".to_string()));
14557
+        assert!(lines
14558
+            .iter()
14559
+            .any(|line| line.contains("compiler_probe_banner: Probe Compiler 7.4")));
14560
+
14561
+        let _ = fs::remove_dir_all(&root);
14562
+    }
14563
+
1428314564
     #[test]
1428414565
     fn case_discovery_lines_distinguish_legacy_surfaces() {
1428514566
         let observable_case = CaseSpec {