Skip to main content

soldb_ethdebug/
artifacts.rs

1//! Finding and loading one contract's debug artifacts from a directory.
2//!
3//! An output directory holds the global resource file (`ethdebug.json` from older
4//! compilers, `ethdebug_resources.json` from modern ones), one program artifact per
5//! contract and environment (`<Contract>_ethdebug.json` for creation code,
6//! `<Contract>_ethdebug-runtime.json` for runtime code), and, for compilers that predate
7//! ETHDebug, a `combined.json` with legacy source maps. [`load_debug_program`] is the one
8//! path through all of that: ETHDebug first, the legacy map as a fallback, and the
9//! source text read from the compilation record or from disk. The CLI and the DAP server
10//! both load through it, so a directory that works in one works in the other.
11
12use std::collections::BTreeMap;
13use std::fs;
14use std::path::{Path, PathBuf};
15
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18
19use soldb_core::{SoldbError, SoldbResult};
20
21use crate::metadata::{read_compilation_source, EthdebugInfo};
22use crate::source_map::{load_source_map_program_with_sources, SourceMapEnvironment};
23use crate::storage_layout::StorageLayout;
24
25/// Which of solc's code generators produced a program.
26///
27/// The two keep variables in different places: the legacy generator reserves a stack slot
28/// per declaration and frees it at the end of its block, which a debugger can follow
29/// without variable information from the compiler; the via-IR pipeline lays the stack out
30/// as its optimizer sees fit, and only compiler-emitted locations can describe it.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33pub enum CodeGenerator {
34    Legacy,
35    ViaIr,
36}
37
38/// One contract's debug information as loaded from its artifacts.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct DebugProgram {
41    pub info: EthdebugInfo,
42    /// The ETHDebug resources record: the compilation, types, and pointers.
43    pub resources: Value,
44    /// Source text by source id, for every source that could be read.
45    pub source_contents: BTreeMap<u64, String>,
46    /// The file each source was read from; a source taken from the compilation record has none.
47    pub source_files: BTreeMap<u64, PathBuf>,
48    /// True when the program came from a legacy `srcmap` rather than ETHDebug.
49    pub legacy: bool,
50    /// The contract's storage layout, when it was compiled with `--storage-layout`.
51    pub storage_layout: Option<StorageLayout>,
52    /// Where its `transient` state variables live, from `--transient-storage-layout`.
53    pub transient_storage_layout: Option<StorageLayout>,
54    /// Sources the artifact names that could not be read, so a frontend can say why its
55    /// lines are missing instead of degrading to an opcode view without a word.
56    pub missing_sources: Vec<String>,
57}
58
59impl DebugProgram {
60    /// The code generator the program came from. A legacy `srcmap` is taken to come from
61    /// the legacy pipeline, which is the only one that emits nothing else; ETHDebug comes
62    /// from solc's via-IR pipeline or from a compiler built on IR.
63    #[must_use]
64    pub const fn code_generator(&self) -> CodeGenerator {
65        if self.legacy {
66            CodeGenerator::Legacy
67        } else {
68            CodeGenerator::ViaIr
69        }
70    }
71
72    /// Fills in the source text for every source the program names, from the compilation
73    /// record first and from disk next to the artifacts otherwise.
74    fn read_sources(&mut self, root: &Path, source_roots: &[PathBuf]) {
75        for (source_id, source_path) in &self.info.sources {
76            if self.source_contents.contains_key(source_id) {
77                continue;
78            }
79            if let Some(source) = read_compilation_source(&self.info.compilation, *source_id) {
80                self.source_contents.insert(*source_id, source);
81                continue;
82            }
83            match read_debug_source_file(root, source_roots, source_path) {
84                Some((file, source)) => {
85                    self.source_contents.insert(*source_id, source);
86                    self.source_files.insert(*source_id, file);
87                }
88                None => self.missing_sources.push(source_path.clone()),
89            }
90        }
91    }
92}
93
94/// The global ETHDebug resource file in `root`, under either of its names.
95#[must_use]
96pub fn find_ethdebug_metadata(root: &Path) -> Option<PathBuf> {
97    // Modern solc (>= ~0.8.32) renamed the global ETHDebug metadata file from
98    // `ethdebug.json` to `ethdebug_resources.json`; accept either.
99    [
100        root.join("ethdebug.json"),
101        root.join("ethdebug_resources.json"),
102    ]
103    .into_iter()
104    .find(|path| path.exists())
105}
106
107/// The ETHDebug program artifact for one contract and environment.
108///
109/// Prefers `<Contract><suffix>`; otherwise accepts a unique artifact whose name ends in
110/// `_<Contract><suffix>`, or the only artifact with that suffix in the directory, which
111/// is how a directory compiled for one contract loads with no name given.
112#[must_use]
113pub fn find_program_ethdebug(
114    root: &Path,
115    contract_name: &str,
116    environment: SourceMapEnvironment,
117) -> Option<PathBuf> {
118    let suffix = program_suffix(environment);
119    let named = root.join(format!("{contract_name}{suffix}"));
120    if !contract_name.is_empty() && named.exists() {
121        return Some(named);
122    }
123
124    let mut candidates = fs::read_dir(root)
125        .ok()?
126        .filter_map(Result::ok)
127        .map(|entry| entry.path())
128        .filter(|path| {
129            path.file_name()
130                .and_then(|name| name.to_str())
131                .is_some_and(|name| name.ends_with(suffix))
132        })
133        .collect::<Vec<_>>();
134    candidates.sort();
135
136    if !contract_name.is_empty() {
137        let contract_suffix = format!("_{contract_name}{suffix}");
138        let mut matching = candidates.iter().filter(|path| {
139            path.file_name()
140                .and_then(|name| name.to_str())
141                .is_some_and(|name| name.ends_with(&contract_suffix))
142        });
143        let matched = matching.next().cloned();
144        if matched.is_some() && matching.next().is_none() {
145            return matched;
146        }
147    }
148
149    (candidates.len() == 1).then(|| candidates.remove(0))
150}
151
152fn program_suffix(environment: SourceMapEnvironment) -> &'static str {
153    match environment {
154        SourceMapEnvironment::Creation => "_ethdebug.json",
155        SourceMapEnvironment::Runtime => "_ethdebug-runtime.json",
156    }
157}
158
159/// The contract name a program artifact's file name carries.
160#[must_use]
161pub fn contract_name_from_program_path(
162    path: &Path,
163    environment: SourceMapEnvironment,
164) -> Option<String> {
165    path.file_name()
166        .and_then(|name| name.to_str())
167        .and_then(|name| name.strip_suffix(program_suffix(environment)))
168        .filter(|name| !name.is_empty())
169        .map(str::to_owned)
170}
171
172/// Reads a source file named by the compiler, trying it relative to the artifact
173/// directory, to that directory's parent, and as given.
174#[must_use]
175pub fn read_debug_source(root: &Path, source_path: &str) -> Option<String> {
176    let (_, contents) = read_debug_source_file(root, &[], source_path)?;
177    Some(contents)
178}
179
180/// The same, looking in `extra_roots` first, which are directories the user named because
181/// the sources are not where the artifact's paths reach, and returning the file it read.
182fn read_debug_source_file(
183    root: &Path,
184    extra_roots: &[PathBuf],
185    source_path: &str,
186) -> Option<(PathBuf, String)> {
187    let candidates = extra_roots
188        .iter()
189        .flat_map(|extra| source_candidates(extra, source_path))
190        .chain(source_candidates(root, source_path));
191    for candidate in candidates {
192        // A candidate that cannot be read is skipped; the next one may be the file.
193        if let Ok(contents) = fs::read_to_string(&candidate) {
194            return Some((candidate, contents));
195        }
196    }
197    None
198}
199
200/// Where a source path recorded in an artifact might be on disk.
201///
202/// A compiler writes the path it was given, which is relative to the directory the
203/// compilation ran in — usually a project root, with the artifacts a few directories
204/// below it. So the artifact directory is tried first, then its ancestors, then the path
205/// as given, which resolves against the directory soldb runs in.
206pub fn source_candidates(root: &Path, source_path: &str) -> Vec<PathBuf> {
207    let source = Path::new(source_path);
208    if source.is_absolute() {
209        return vec![source.to_path_buf()];
210    }
211    let mut candidates = Vec::new();
212    let mut directory = Some(root);
213    // The project root is rarely more than a few directories above `out/`.
214    for _ in 0..4 {
215        let Some(current) = directory else {
216            break;
217        };
218        candidates.push(current.join(source));
219        directory = current.parent();
220    }
221    candidates.push(source.to_path_buf());
222    candidates
223}
224
225/// Parses a JSON file, naming the file in the error.
226pub fn read_json_file(path: &Path) -> SoldbResult<Value> {
227    let content = fs::read_to_string(path).map_err(|error| {
228        SoldbError::Message(format!("failed to read `{}`: {error}", path.display()))
229    })?;
230    serde_json::from_str(&content).map_err(|error| {
231        SoldbError::Message(format!("invalid JSON in `{}`: {error}", path.display()))
232    })
233}
234
235/// The resources record of an ETHDebug metadata file: its `resources` entry when it has
236/// one, otherwise one assembled from the compilation, types, and pointers it carries.
237pub fn ethdebug_resources_from_metadata(path: &Path, metadata: &Value) -> SoldbResult<Value> {
238    if let Some(resources) = metadata.get("resources") {
239        return Ok(resources.clone());
240    }
241
242    let Some(compilation) = metadata.get("compilation") else {
243        return Err(SoldbError::Message(format!(
244            "ETHDebug metadata {} does not contain resources or compilation",
245            path.display()
246        )));
247    };
248
249    Ok(json!({
250        "compilation": compilation,
251        "types": metadata.get("types").cloned().unwrap_or_else(|| json!({})),
252        "pointers": metadata.get("pointers").cloned().unwrap_or_else(|| json!({})),
253    }))
254}
255
256/// Loads a contract's debug program from `root`: the ETHDebug artifacts when present,
257/// otherwise the legacy source map in `combined.json`.
258///
259/// `Ok(None)` means the directory holds neither for this contract. A present but
260/// unreadable artifact is an error, so a caller never silently loses source attribution.
261/// An empty `contract_name` loads the only program in the directory and takes its name
262/// from the file.
263pub fn load_debug_program(
264    root: &Path,
265    contract_name: &str,
266    environment: SourceMapEnvironment,
267) -> SoldbResult<Option<DebugProgram>> {
268    load_debug_program_with_sources(root, contract_name, environment, &[])
269}
270
271/// The same, also looking for sources in `source_roots`.
272///
273/// A compiler records the source paths it was given, which are relative to the directory
274/// the compilation ran in. When the artifacts are somewhere else entirely, that directory
275/// is the one thing the debugger cannot work out for itself, so a frontend passes it.
276pub fn load_debug_program_with_sources(
277    root: &Path,
278    contract_name: &str,
279    environment: SourceMapEnvironment,
280    source_roots: &[PathBuf],
281) -> SoldbResult<Option<DebugProgram>> {
282    let environment_name = match environment {
283        SourceMapEnvironment::Creation => "create",
284        SourceMapEnvironment::Runtime => "call",
285    };
286    if let Some(program_path) = find_program_ethdebug(root, contract_name, environment) {
287        let metadata_path = find_ethdebug_metadata(root).ok_or_else(|| {
288            SoldbError::Message(format!(
289                "No ETHDebug metadata file (ethdebug.json or ethdebug_resources.json) found in {}",
290                root.display()
291            ))
292        })?;
293        let metadata = read_json_file(&metadata_path)?;
294        let program = read_json_file(&program_path)?;
295        let resources = ethdebug_resources_from_metadata(&metadata_path, &metadata)?;
296        let name = if contract_name.is_empty() {
297            contract_name_from_program_path(&program_path, environment)
298                .unwrap_or_else(|| "Contract".to_owned())
299        } else {
300            contract_name.to_owned()
301        };
302        let info = EthdebugInfo::from_artifacts(&name, environment_name, &metadata, &program)
303            .map_err(|error| SoldbError::Message(format!("{}: {error}", program_path.display())))?;
304        let storage_layout = load_storage_layout(root, &info.contract_name)?;
305        let transient_storage_layout = load_transient_storage_layout(root, &info.contract_name)?;
306        let mut program = DebugProgram {
307            info,
308            resources,
309            source_contents: BTreeMap::new(),
310            source_files: BTreeMap::new(),
311            legacy: false,
312            storage_layout,
313            transient_storage_layout,
314            missing_sources: Vec::new(),
315        };
316        program.read_sources(root, source_roots);
317        return Ok(Some(program));
318    }
319
320    let Some(program) =
321        load_source_map_program_with_sources(root, source_roots, contract_name, environment)?
322    else {
323        return Ok(None);
324    };
325    let storage_layout = match program.storage_layout {
326        Some(layout) => Some(layout),
327        None => load_storage_layout(root, &program.info.contract_name)?,
328    };
329    let transient_storage_layout = match program.transient_storage_layout {
330        Some(layout) => Some(layout),
331        None => load_transient_storage_layout(root, &program.info.contract_name)?,
332    };
333    let mut program = DebugProgram {
334        info: program.info,
335        resources: program.resources,
336        source_contents: program.source_contents,
337        source_files: program.source_files,
338        legacy: true,
339        storage_layout,
340        transient_storage_layout,
341        missing_sources: Vec::new(),
342    };
343    program.read_sources(root, source_roots);
344    Ok(Some(program))
345}
346
347/// The `<Contract>_storage.json` that `solc --storage-layout -o <root>` writes, when it
348/// is there. A missing file is not an error: the layout is optional debug information.
349/// An unreadable one is, because the user compiled with it and would otherwise get a
350/// silent "no storage layout" that looks identical to never having asked for it.
351pub fn load_storage_layout(root: &Path, contract_name: &str) -> SoldbResult<Option<StorageLayout>> {
352    load_layout_file(&root.join(format!("{contract_name}_storage.json")))
353}
354
355/// `<Contract>_transient_storage.json`, under the same rules as [`load_storage_layout`].
356pub fn load_transient_storage_layout(
357    root: &Path,
358    contract_name: &str,
359) -> SoldbResult<Option<StorageLayout>> {
360    load_layout_file(&root.join(format!("{contract_name}_transient_storage.json")))
361}
362
363fn load_layout_file(path: &Path) -> SoldbResult<Option<StorageLayout>> {
364    if !path.exists() {
365        return Ok(None);
366    }
367    let value = read_json_file(path)?;
368    StorageLayout::parse(&value)
369        .map(Some)
370        .map_err(|error| SoldbError::Message(format!("{}: {error}", path.display())))
371}
372
373/// One entry of a contract mapping file: a deployed address and the directory holding
374/// the artifacts that describe its code.
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct ContractMapping {
377    pub address: String,
378    pub name: String,
379    pub debug_dir: PathBuf,
380    /// The keccak-256 hash of the code the artifacts describe, when the file names it, so
381    /// that two programs run at one address keep their own artifacts.
382    pub code_hash: Option<String>,
383}
384
385/// Reads a contract mapping file, `{"contracts": [{"address", "name", "debug_dir"}]}`,
386/// where an entry may also name its `code_hash`.
387pub fn load_contract_mapping(path: &Path) -> SoldbResult<Vec<ContractMapping>> {
388    let value = read_json_file(path)?;
389    parse_contract_mapping(path, &value)
390}
391
392/// The entries of a parsed mapping file. A relative `debug_dir` is relative to the file.
393pub fn parse_contract_mapping(path: &Path, value: &Value) -> SoldbResult<Vec<ContractMapping>> {
394    let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
395    let contracts = value
396        .get("contracts")
397        .and_then(Value::as_array)
398        .ok_or_else(|| {
399            SoldbError::Message(format!(
400                "contract mapping `{}` must contain a `contracts` array",
401                path.display()
402            ))
403        })?;
404
405    Ok(contracts
406        .iter()
407        .filter_map(|contract| {
408            let address = contract.get("address")?.as_str()?.to_owned();
409            let name = contract
410                .get("name")
411                .and_then(Value::as_str)
412                .unwrap_or("Unknown")
413                .to_owned();
414            let debug_dir = contract
415                .get("debug_dir")
416                .and_then(Value::as_str)
417                .map(PathBuf::from)?;
418            let debug_dir = if debug_dir.is_absolute() {
419                debug_dir
420            } else {
421                base_dir.join(debug_dir)
422            };
423            let code_hash = contract
424                .get("code_hash")
425                .and_then(Value::as_str)
426                .map(str::to_owned);
427            Some(ContractMapping {
428                address,
429                name,
430                debug_dir,
431                code_hash,
432            })
433        })
434        .collect())
435}
436
437#[cfg(test)]
438mod tests {
439    use std::fs;
440    use std::path::PathBuf;
441    use std::time::{SystemTime, UNIX_EPOCH};
442
443    use serde_json::json;
444
445    use super::{
446        contract_name_from_program_path, ethdebug_resources_from_metadata, find_ethdebug_metadata,
447        find_program_ethdebug, load_debug_program, load_storage_layout,
448        load_transient_storage_layout, read_debug_source, read_json_file,
449    };
450    use crate::source_map::SourceMapEnvironment;
451
452    fn temp_dir(label: &str) -> PathBuf {
453        let nanos = SystemTime::now()
454            .duration_since(UNIX_EPOCH)
455            .expect("clock")
456            .as_nanos();
457        let dir = std::env::temp_dir().join(format!("soldb-artifacts-{label}-{nanos}"));
458        fs::create_dir_all(&dir).expect("create temp dir");
459        dir
460    }
461
462    #[test]
463    fn loads_ethdebug_programs_with_their_sources() {
464        let dir = temp_dir("ethdebug");
465        let source =
466            "contract Counter {\n  function set(uint256 x) public {\n    value = x;\n  }\n}\n";
467        fs::write(dir.join("Counter.sol"), source).expect("source");
468        fs::write(
469            dir.join("ethdebug_resources.json"),
470            json!({"compilation": {"sources": [{"id": 0, "path": "Counter.sol"}]}}).to_string(),
471        )
472        .expect("metadata");
473        fs::write(
474            dir.join("Counter_ethdebug-runtime.json"),
475            json!({"instructions": [{
476                "offset": 7,
477                "operation": {"mnemonic": "STOP"},
478                "context": {"code": {"source": {"id": 0}, "range": {"offset": 59, "length": 9}}}
479            }]})
480            .to_string(),
481        )
482        .expect("program");
483
484        let program = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
485            .expect("load")
486            .expect("present");
487        assert!(!program.legacy);
488        assert_eq!(program.info.contract_name, "Counter");
489        assert_eq!(program.info.environment, "call");
490        assert!(program.info.instruction_at_pc(7).is_some());
491        assert_eq!(
492            program.source_contents.get(&0).map(String::as_str),
493            Some(source)
494        );
495        assert_eq!(program.source_files.get(&0), Some(&dir.join("Counter.sol")));
496        assert!(program.resources.get("compilation").is_some());
497        // No `Counter_storage.json` yet: the layout is optional debug information.
498        assert!(program.storage_layout.is_none());
499
500        // No name: the only program in the directory, named after its file.
501        let inferred = load_debug_program(&dir, "", SourceMapEnvironment::Runtime)
502            .expect("load")
503            .expect("present");
504        assert_eq!(inferred.info.contract_name, "Counter");
505        // Creation code is a different artifact, absent here.
506        assert!(
507            load_debug_program(&dir, "Counter", SourceMapEnvironment::Creation)
508                .expect("load")
509                .is_none()
510        );
511        assert_eq!(
512            contract_name_from_program_path(
513                &dir.join("Counter_ethdebug-runtime.json"),
514                SourceMapEnvironment::Runtime
515            )
516            .as_deref(),
517            Some("Counter")
518        );
519        assert_eq!(
520            find_ethdebug_metadata(&dir),
521            Some(dir.join("ethdebug_resources.json"))
522        );
523        assert_eq!(
524            find_program_ethdebug(&dir, "Other", SourceMapEnvironment::Runtime),
525            Some(dir.join("Counter_ethdebug-runtime.json")),
526            "a single artifact serves any name"
527        );
528        assert_eq!(
529            read_debug_source(&dir, "Counter.sol").as_deref(),
530            Some(source)
531        );
532        assert!(read_debug_source(&dir, "Missing.sol").is_none());
533
534        // `solc --storage-layout -o <dir>` writes one file per contract; the loader picks
535        // it up, and a malformed one is an error rather than a silent miss.
536        fs::write(
537            dir.join("Counter_storage.json"),
538            json!({
539                "storage": [{
540                    "astId": 1,
541                    "contract": "Counter.sol:Counter",
542                    "label": "value",
543                    "offset": 0,
544                    "slot": "1",
545                    "type": "t_uint256"
546                }],
547                "types": {"t_uint256": {
548                    "encoding": "inplace",
549                    "label": "uint256",
550                    "numberOfBytes": "32"
551                }}
552            })
553            .to_string(),
554        )
555        .expect("layout");
556        let program = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
557            .expect("load")
558            .expect("present");
559        let layout = program.storage_layout.as_ref().expect("storage layout");
560        assert_eq!(layout.variable("value").expect("value").slot[31], 1);
561        assert!(load_storage_layout(&dir, "Missing")
562            .expect("absent")
563            .is_none());
564        // Not compiled with `--transient-storage-layout`: no transient layout.
565        assert!(program.transient_storage_layout.is_none());
566
567        // `--transient-storage-layout` writes a second file of the same shape, for the
568        // `transient` variables, whose slots number from zero in their own space.
569        fs::write(
570            dir.join("Counter_transient_storage.json"),
571            json!({
572                "storage": [{
573                    "astId": 2,
574                    "contract": "Counter.sol:Counter",
575                    "label": "locked",
576                    "offset": 0,
577                    "slot": "0",
578                    "type": "t_bool"
579                }],
580                "types": {"t_bool": {
581                    "encoding": "inplace",
582                    "label": "bool",
583                    "numberOfBytes": "1"
584                }}
585            })
586            .to_string(),
587        )
588        .expect("transient layout");
589        let program = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
590            .expect("load")
591            .expect("present");
592        let transient = program
593            .transient_storage_layout
594            .as_ref()
595            .expect("transient layout");
596        assert_eq!(transient.variable("locked").expect("locked").slot, [0; 32]);
597        assert!(transient.variable("value").is_none());
598        assert!(load_transient_storage_layout(&dir, "Missing")
599            .expect("absent")
600            .is_none());
601        fs::write(dir.join("Counter_storage.json"), "{}").expect("layout");
602        let error = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
603            .expect_err("a storage layout that cannot be read is reported");
604        assert!(
605            error.to_string().contains("Counter_storage.json"),
606            "{error}"
607        );
608    }
609
610    #[test]
611    fn a_program_without_metadata_is_an_error_not_a_silent_miss() {
612        let dir = temp_dir("no-metadata");
613        fs::write(dir.join("Counter_ethdebug-runtime.json"), "{}").expect("program");
614        let error = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
615            .expect_err("metadata is required");
616        assert!(
617            error.to_string().contains("No ETHDebug metadata file"),
618            "{error}"
619        );
620
621        fs::write(dir.join("ethdebug.json"), "{not json").expect("metadata");
622        let error = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
623            .expect_err("malformed metadata");
624        assert!(error.to_string().contains("invalid JSON"), "{error}");
625        assert!(read_json_file(&dir.join("absent.json")).is_err());
626    }
627
628    #[test]
629    fn falls_back_to_legacy_source_maps() {
630        let dir = temp_dir("legacy");
631        let source =
632            "contract Counter {\n  function set(uint256 x) public {\n    value = x;\n  }\n}\n";
633        fs::write(dir.join("Counter.sol"), source).expect("source");
634        let offset = source.find("value = x").expect("statement");
635        fs::write(
636            dir.join("combined.json"),
637            json!({
638                "sourceList": ["Counter.sol"],
639                "contracts": {"Counter.sol:Counter": {
640                    "bin-runtime": "60010000",
641                    "srcmap-runtime": format!("{offset}:9:0:-:0;{offset}:9:0;{offset}:9:0")
642                }}
643            })
644            .to_string(),
645        )
646        .expect("combined");
647
648        let program = load_debug_program(&dir, "Counter", SourceMapEnvironment::Runtime)
649            .expect("load")
650            .expect("present");
651        assert!(program.legacy);
652        assert_eq!(
653            program.source_contents.get(&0).map(String::as_str),
654            Some(source)
655        );
656        assert_eq!(program.source_files.get(&0), Some(&dir.join("Counter.sol")));
657        assert!(program.info.instruction_at_pc(2).is_some());
658
659        let empty = temp_dir("empty");
660        assert!(
661            load_debug_program(&empty, "Counter", SourceMapEnvironment::Runtime)
662                .expect("load")
663                .is_none()
664        );
665    }
666
667    #[test]
668    fn resources_come_from_the_record_or_are_assembled() {
669        let path = PathBuf::from("ethdebug.json");
670        let explicit = json!({"resources": {"compilation": {}}});
671        assert_eq!(
672            ethdebug_resources_from_metadata(&path, &explicit).expect("resources"),
673            json!({"compilation": {}})
674        );
675        let assembled = json!({"compilation": {"sources": []}, "types": {"a": 1}});
676        assert_eq!(
677            ethdebug_resources_from_metadata(&path, &assembled).expect("resources"),
678            json!({"compilation": {"sources": []}, "types": {"a": 1}, "pointers": {}})
679        );
680        let error = ethdebug_resources_from_metadata(&path, &json!({})).expect_err("neither");
681        assert!(
682            error.to_string().contains("does not contain resources"),
683            "{error}"
684        );
685    }
686}