Skip to main content

forge/cmd/
coverage.rs

1use super::{
2    test::{ProjectPathsAwareFilter, TestArgs, TestExecutionOptions},
3    watch::WatchArgs,
4};
5use crate::coverage::{
6    BytecodeReporter, ContractId, CoverageAttributionReporter, CoverageReport, CoverageReporter,
7    CoverageSummaryReporter, DebugReporter, ItemAnchors, LcovReporter, ResolvedHitMap,
8    ResolvedHitMaps,
9    analysis::{SourceAnalysis, SourceFiles},
10    anchors::{find_anchors, find_execution_anchors},
11};
12use alloy_json_abi::StateMutability;
13use alloy_primitives::{Address, Bytes, U256, map::HashMap};
14use clap::{Parser, ValueHint};
15use eyre::Result;
16use foundry_cli::utils::{FoundryPathExt, LoadConfig, STATIC_FUZZ_SEED};
17use foundry_common::{TestFilter, compile::ProjectCompiler, errors::convert_solar_errors};
18use foundry_compilers::{
19    Artifact, ArtifactId, Project, ProjectCompileOutput, ProjectPathsConfig, VYPER_EXTENSIONS,
20    artifacts::{CompactBytecode, CompactDeployedBytecode, sourcemap::SourceMap},
21    compilers::{Language, multi::MultiCompilerLanguage},
22    utils::source_files_iter,
23};
24use foundry_config::{
25    Config, CoverageConfig, CoverageReportKind, InlineConfig, parse_lcov_version,
26};
27use foundry_evm::{core::ic::IcPcMap, opts::EvmOpts};
28use globset::{Glob, GlobSetBuilder};
29use rayon::prelude::*;
30use semver::Version;
31use std::{
32    collections::BTreeSet,
33    path::{Path, PathBuf},
34    sync::Arc,
35};
36
37// Loads project's figment and merges the build cli arguments into it
38foundry_config::impl_figment_convert!(CoverageArgs, test);
39
40/// CLI arguments for `forge coverage`.
41///
42/// Most flags here have a corresponding `[profile.<name>.coverage]` config
43/// option in `foundry.toml`. CLI flags take precedence over config; the helper
44/// `resolve_with` merges them after the config is loaded.
45#[derive(Parser)]
46#[command(after_long_help = r#"Source attribution:
47  Coverage follows compiler source maps. Inherited modifier code is reported under the
48  source where the modifier is declared. Dependency sources are excluded by default;
49  use `--include-libs` to include their coverage.
50
51Compatibility:
52  `forge coverage` supports test filters and `--watch`, but not test-only output or
53  execution modes such as `--json`, `--junit`, `--list`, `--debug`, flame profiles,
54  symbolic artifact replay, showmap replay, brutalization, or mutation testing. Use
55  `--report lcov` for interoperable coverage data or `--report attribution` for
56  Foundry's per-test JSON attribution report."#)]
57pub struct CoverageArgs {
58    /// The report type to use for coverage.
59    ///
60    /// This flag can be used multiple times. Falls back to the
61    /// `[profile.<name>.coverage] report` config value when not provided
62    /// (default: `summary`).
63    #[arg(long, value_enum)]
64    report: Vec<CoverageReportKind>,
65
66    /// The version of the LCOV "tracefile" format to use.
67    ///
68    /// Format: `MAJOR[.MINOR]`.
69    ///
70    /// Main differences:
71    /// - `1.x`: The original v1 format.
72    /// - `2.0`: Adds support for "line end" numbers for functions.
73    /// - `2.2`: Changes the format of functions.
74    ///
75    /// Falls back to the `[profile.<name>.coverage] lcov_version` config value
76    /// when not provided.
77    #[arg(long = "lcov-version", value_parser = parse_lcov_version)]
78    lcov_version_cli: Option<Version>,
79
80    /// The resolved LCOV version to use after merging CLI and config values.
81    #[arg(skip = Version::new(1, 0, 0))]
82    lcov_version: Version,
83
84    /// Enable viaIR with minimum optimization
85    ///
86    /// This can fix most of the "stack too deep" errors while resulting a
87    /// relatively accurate source map.
88    #[arg(long)]
89    ir_minimum: bool,
90
91    /// The path to output the report.
92    ///
93    /// Used only when a single file report is requested. If not specified, the
94    /// report will be stored in the root of the project.
95    #[arg(
96        long,
97        value_hint = ValueHint::FilePath,
98        value_name = "PATH"
99    )]
100    report_file: Option<PathBuf>,
101
102    /// Include dependency sources in the coverage report.
103    #[arg(long)]
104    include_libs: bool,
105
106    /// Whether to exclude tests from the coverage report.
107    #[arg(long)]
108    exclude_tests: bool,
109
110    /// The coverage reporters to use. Constructed from the other fields.
111    #[arg(skip)]
112    reporters: Vec<Box<dyn CoverageReporter>>,
113
114    /// Glob patterns of source files to exclude from the coverage report.
115    /// Populated from `[profile.<name>.coverage] skip_files` after config is
116    /// loaded; not exposed directly on the CLI.
117    #[arg(skip)]
118    skip_files: Vec<String>,
119
120    #[command(flatten)]
121    test: TestArgs,
122}
123
124impl CoverageArgs {
125    fn report_path(&self, root: &Path, default_file_name: &str) -> PathBuf {
126        let report_file =
127            (self.file_report_count() == 1).then_some(self.report_file.as_deref()).flatten();
128        root.join(report_file.unwrap_or_else(|| Path::new(default_file_name)))
129    }
130
131    fn file_report_count(&self) -> usize {
132        let has_lcov = self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Lcov));
133        let has_attribution =
134            self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Attribution));
135        usize::from(has_lcov) + usize::from(has_attribution)
136    }
137
138    pub(crate) fn ensure_mode_compatible(&self) -> Result<()> {
139        self.test.ensure_coverage_mode_compatible()
140    }
141
142    pub async fn run(mut self) -> Result<()> {
143        self.ensure_mode_compatible()?;
144
145        let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
146
147        // install missing dependencies
148        self.install_missing_dependencies(&mut config)?;
149
150        // Default to a static fuzz seed so coverage reports are deterministic,
151        // but allow the user to override it via `--fuzz-seed` or `[fuzz] seed` in config.
152        if config.fuzz.seed.is_none() {
153            config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED));
154        }
155
156        // Merge CLI args with `[profile.<name>.coverage]` config values. CLI
157        // flags take precedence; unset CLI flags fall back to the config.
158        self.resolve_with(&config.coverage);
159        let filter = self.test.filter(&config)?;
160
161        let (paths, mut output) = {
162            let (project, output) = self.build(&config, &filter)?;
163            (project.paths, output)
164        };
165
166        if self.report_file.is_some() && self.file_report_count() > 1 {
167            sh_warn!(
168                "`--report-file` is ignored when multiple file reports are requested; \
169                 each report will use its default output path"
170            )?;
171        }
172
173        self.populate_reporters(&paths.root);
174
175        sh_println!("Analysing contracts...")?;
176        let report = self.prepare(&paths, &mut output)?;
177
178        sh_println!("Running tests...")?;
179        self.collect(&paths.root, &output, report, config, evm_opts, filter).await
180    }
181
182    /// Merge `[profile.<name>.coverage]` config values into this struct. CLI
183    /// flags already set on `self` win; unset/false flags inherit from
184    /// `config`.
185    ///
186    /// After this returns:
187    /// - `self.report` is non-empty.
188    /// - boolean flags reflect `cli || config` (CLI cannot disable a flag set to `true` in config;
189    ///   this matches the pre-existing flag-only semantics where booleans defaulted to `false`).
190    fn resolve_with(&mut self, config: &CoverageConfig) {
191        if self.report.is_empty() {
192            self.report.clone_from(&config.report);
193        }
194        self.lcov_version =
195            self.lcov_version_cli.clone().unwrap_or_else(|| config.lcov_version.clone());
196        if !self.ir_minimum {
197            self.ir_minimum = config.ir_minimum;
198        }
199        if self.report_file.is_none() {
200            self.report_file.clone_from(&config.report_file);
201        }
202        if !self.include_libs {
203            self.include_libs = config.include_libs;
204        }
205        if !self.exclude_tests {
206            self.exclude_tests = config.exclude_tests;
207        }
208        // Glob filters are additive — there's no CLI flag for these, so always
209        // take from config.
210        self.skip_files.clone_from(&config.skip_files);
211    }
212
213    fn populate_reporters(&mut self, root: &Path) {
214        self.reporters = self
215            .report
216            .iter()
217            .filter_map(|report_kind| match report_kind {
218                CoverageReportKind::Summary => {
219                    Some(Box::<CoverageSummaryReporter>::default() as Box<dyn CoverageReporter>)
220                }
221                CoverageReportKind::Lcov => {
222                    let path = self.report_path(root, "lcov.info");
223                    Some(Box::new(LcovReporter::new(path, self.lcov_version.clone())))
224                }
225                CoverageReportKind::Bytecode => Some(Box::new(BytecodeReporter::new(
226                    root.to_path_buf(),
227                    root.join("bytecode-coverage"),
228                ))),
229                CoverageReportKind::Debug => Some(Box::new(DebugReporter)),
230                CoverageReportKind::Attribution => None,
231            })
232            .collect::<Vec<_>>();
233    }
234
235    /// Builds the project.
236    fn build(
237        &self,
238        config: &Config,
239        filter: &ProjectPathsAwareFilter,
240    ) -> Result<(Project, ProjectCompileOutput)> {
241        let mut project = config.ephemeral_project()?;
242
243        if self.ir_minimum {
244            sh_warn!(
245                "`--ir-minimum` enables `viaIR` with minimum optimization, \
246                 which can result in inaccurate source mappings.\n\
247                 Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n\
248                 Note that `viaIR` is production ready since Solidity 0.8.13 and above.\n\
249                 See more: https://book.getfoundry.sh/guides/best-practices/stack-too-deep"
250            )?;
251        } else {
252            sh_warn!(
253                "optimizer settings and `viaIR` have been disabled for accurate coverage reports.\n\
254                 If you encounter \"stack too deep\" errors, consider using `--ir-minimum` which \
255                 enables `viaIR` with minimum optimization resolving most of the errors.\n\
256                 See more: https://book.getfoundry.sh/guides/best-practices/stack-too-deep"
257            )?;
258        }
259
260        config.disable_optimizations(&mut project, self.ir_minimum);
261
262        let mut compiler = ProjectCompiler::new().dynamic_test_linking(config.dynamic_test_linking);
263        if filter.args().path_pattern.is_some() || filter.args().path_pattern_inverse.is_some() {
264            let sources = source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
265                .chain(
266                    source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS)
267                        // Preserve path-filter behavior for conventional test files while still
268                        // scanning non-test fixtures under the test root.
269                        .filter(|path| !path.is_sol_test() || filter.matches_path(path)),
270                )
271                // Coverage reports include scripts even though they are not test targets.
272                .chain(source_files_iter(&config.script, MultiCompilerLanguage::FILE_EXTENSIONS))
273                .collect::<BTreeSet<_>>();
274            compiler = compiler.files(sources);
275        }
276        let output = compiler.compile(&project)?.with_stripped_file_prefixes(project.root());
277
278        Ok((project, output))
279    }
280
281    /// Builds the coverage report.
282    #[instrument(name = "Coverage::prepare", skip_all)]
283    fn prepare(
284        &self,
285        project_paths: &ProjectPathsConfig,
286        output: &mut ProjectCompileOutput,
287    ) -> Result<CoverageReport> {
288        let mut report = CoverageReport::default();
289
290        output.parser_mut().solc_mut().compiler_mut().enter_mut(|compiler| {
291            if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) {
292                let _ = compiler.lower_asts();
293            }
294            convert_solar_errors(compiler.dcx())
295        })?;
296        let output = &*output;
297
298        // Collect source files.
299        let mut sources_by_build = HashMap::<String, SourceFiles>::default();
300        for (path, sources) in &output.output().sources.0 {
301            // Filter out vyper sources.
302            if path
303                .extension()
304                .and_then(|s| s.to_str())
305                .is_some_and(|ext| VYPER_EXTENSIONS.contains(&ext))
306            {
307                continue;
308            }
309
310            for source in sources {
311                let source_file = &source.source_file;
312                report.add_source(source.build_id.clone(), source_file.id as usize, path.clone());
313
314                // Filter out libs dependencies and tests.
315                if (!self.include_libs && project_paths.has_library_ancestor(path))
316                    || (self.exclude_tests && project_paths.is_test(path))
317                {
318                    continue;
319                }
320
321                sources_by_build
322                    .entry(source.build_id.clone())
323                    .or_default()
324                    .sources
325                    .insert(source_file.id, project_paths.root.join(path));
326            }
327        }
328
329        // Get source maps and bytecodes.
330        let artifacts: Vec<ArtifactData> = output
331            .artifact_ids()
332            .par_bridge() // This parses source maps, so we want to run it in parallel.
333            .filter_map(|(id, artifact)| {
334                let source_id = report.get_source_id(&id.build_id, &id.source)?;
335                ArtifactData::new(&id, source_id, artifact)
336            })
337            .collect();
338
339        // Add coverage items.
340        for (build_id, sources) in &sources_by_build {
341            let source_analysis = SourceAnalysis::new(sources, output)?;
342            let anchors = artifacts
343                .par_iter()
344                .filter(|artifact| artifact.contract_id.build_id == *build_id)
345                .map(|artifact| {
346                    let creation_code_anchors = artifact.creation.find_anchors(&source_analysis);
347                    let deployed_code_anchors = artifact.deployed.find_anchors(&source_analysis);
348                    (artifact.contract_id.clone(), (creation_code_anchors, deployed_code_anchors))
349                })
350                .collect_vec_list();
351            report.add_anchors(anchors.into_iter().flatten());
352            for artifact in
353                artifacts.iter().filter(|artifact| artifact.contract_id.build_id == *build_id)
354            {
355                let execution_anchors = find_execution_anchors(
356                    artifact.contract_id.source_id as u32,
357                    &artifact.contract_id.contract_name,
358                    &source_analysis,
359                );
360                report.add_execution_anchors(
361                    artifact.contract_id.clone(),
362                    execution_anchors,
363                    artifact.function_selectors.iter().copied(),
364                    artifact.has_receive,
365                    artifact.fallback_payable,
366                );
367            }
368            report.add_analysis(build_id.clone(), source_analysis);
369        }
370
371        if self.reporters.iter().any(|reporter| reporter.needs_source_maps()) {
372            report.add_source_maps(artifacts.into_iter().map(|artifact| {
373                (artifact.contract_id, (artifact.creation.source_map, artifact.deployed.source_map))
374            }));
375        }
376
377        Ok(report)
378    }
379
380    /// Runs tests, collects coverage data and generates the final report.
381    #[instrument(name = "Coverage::collect", skip_all)]
382    async fn collect(
383        mut self,
384        project_root: &Path,
385        output: &ProjectCompileOutput,
386        mut report: CoverageReport,
387        config: Config,
388        evm_opts: EvmOpts,
389        filter: ProjectPathsAwareFilter,
390    ) -> Result<()> {
391        let inline_config = Arc::new(InlineConfig::new_parsed(output, &config)?);
392        let outcome = self
393            .test
394            .run_tests(
395                project_root,
396                config,
397                evm_opts,
398                output,
399                &filter,
400                TestExecutionOptions::coverage(inline_config),
401            )
402            .await?;
403
404        let known_contracts = outcome.known_contracts.as_ref().unwrap();
405        let mut resolved_hit_maps = ResolvedHitMaps::default();
406
407        // Add hit data to the coverage report
408        for suite in outcome.results.values() {
409            for result in suite.test_results.values() {
410                let Some(hit_maps) = result.line_coverage.as_ref() else { continue };
411
412                for (code_hash, map) in &hit_maps.0 {
413                    if let Some(resolved) = resolved_hit_maps.get(code_hash) {
414                        report.add_hit_map(
415                            &resolved.contract_id,
416                            map,
417                            resolved.is_deployed_code,
418                        )?;
419                        continue;
420                    }
421
422                    let Some((artifact_id, is_deployed_code)) = known_contracts
423                        .find_by_deployed_code(map.bytecode())
424                        .map(|(id, _)| (id, true))
425                        .or_else(|| {
426                            known_contracts
427                                .find_by_creation_code(map.bytecode())
428                                .map(|(id, _)| (id, false))
429                        })
430                    else {
431                        continue;
432                    };
433
434                    let Some(source_id) =
435                        report.get_source_id(&artifact_id.build_id, &artifact_id.source)
436                    else {
437                        continue;
438                    };
439                    let contract_id = ContractId {
440                        version: artifact_id.version.clone(),
441                        build_id: artifact_id.build_id.clone(),
442                        source_id,
443                        contract_name: artifact_id.name.as_str().into(),
444                    };
445
446                    report.add_hit_map(&contract_id, map, is_deployed_code)?;
447
448                    resolved_hit_maps
449                        .entry(*code_hash)
450                        .or_insert(ResolvedHitMap { contract_id, is_deployed_code });
451                }
452            }
453        }
454
455        // Filter out ignored sources from the report.
456        let file_root = filter.paths().root.as_path();
457        if let Some(not_re) = &filter.args().coverage_pattern_inverse {
458            report.retain_sources(|path: &Path| {
459                let path = path.strip_prefix(file_root).unwrap_or(path);
460                !not_re.is_match(&path.to_string_lossy())
461            });
462        }
463        if !self.skip_files.is_empty() {
464            let mut builder = GlobSetBuilder::new();
465            for pattern in &self.skip_files {
466                let glob = Glob::new(pattern).map_err(|e| {
467                    eyre::eyre!("invalid glob in coverage.skip_files: '{pattern}': {e}")
468                })?;
469                builder.add(glob);
470            }
471            let set = builder
472                .build()
473                .map_err(|e| eyre::eyre!("failed to build coverage.skip_files glob set: {e}"))?;
474            report.retain_sources(|path: &Path| {
475                let path = path.strip_prefix(file_root).unwrap_or(path);
476                !set.is_match(path)
477            });
478        }
479
480        // Output final reports.
481        self.report(&report)?;
482
483        if self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Attribution)) {
484            let reporter = CoverageAttributionReporter::new(
485                self.report_path(project_root, "coverage-attribution.json"),
486            );
487            reporter.report(&report, &outcome, &resolved_hit_maps)?;
488        }
489
490        // Check for test failures after generating coverage report.
491        // This ensures coverage data is written even when tests fail.
492        outcome.ensure_ok(false)?;
493
494        Ok(())
495    }
496
497    #[instrument(name = "Coverage::report", skip_all)]
498    fn report(&mut self, report: &CoverageReport) -> Result<()> {
499        for reporter in &mut self.reporters {
500            let _guard = debug_span!("reporter.report", kind=%reporter.name()).entered();
501            reporter.report(report)?;
502        }
503        Ok(())
504    }
505
506    pub const fn is_watch(&self) -> bool {
507        self.test.is_watch()
508    }
509
510    pub const fn watch(&self) -> &WatchArgs {
511        &self.test.watch
512    }
513}
514
515/// Helper function that will link references in unlinked bytecode to the 0 address.
516///
517/// This is needed in order to analyze the bytecode for contracts that use libraries.
518fn dummy_link_bytecode(mut obj: CompactBytecode) -> Option<Bytes> {
519    let link_references = obj.link_references.clone();
520    for (file, libraries) in link_references {
521        for library in libraries.keys() {
522            obj.link(&file, library, Address::ZERO);
523        }
524    }
525
526    obj.object.resolve();
527    obj.object.into_bytes()
528}
529
530/// Helper function that will link references in unlinked bytecode to the 0 address.
531///
532/// This is needed in order to analyze the bytecode for contracts that use libraries.
533fn dummy_link_deployed_bytecode(obj: CompactDeployedBytecode) -> Option<Bytes> {
534    obj.bytecode.and_then(dummy_link_bytecode)
535}
536
537pub struct ArtifactData {
538    pub contract_id: ContractId,
539    pub creation: BytecodeData,
540    pub deployed: BytecodeData,
541    pub function_selectors: Vec<[u8; 4]>,
542    pub has_receive: bool,
543    pub fallback_payable: bool,
544}
545
546impl ArtifactData {
547    pub fn new(id: &ArtifactId, source_id: usize, artifact: &impl Artifact) -> Option<Self> {
548        let abi = artifact.get_abi();
549        let function_selectors = abi
550            .as_ref()
551            .map(|abi| abi.functions().map(|function| function.selector().into()).collect())
552            .unwrap_or_default();
553        let has_receive = abi.as_ref().is_some_and(|abi| abi.receive.is_some());
554        let fallback_payable = abi
555            .as_ref()
556            .and_then(|abi| abi.fallback)
557            .is_some_and(|fallback| fallback.state_mutability == StateMutability::Payable);
558        Some(Self {
559            contract_id: ContractId {
560                version: id.version.clone(),
561                build_id: id.build_id.clone(),
562                source_id,
563                contract_name: id.name.as_str().into(),
564            },
565            creation: BytecodeData::new(
566                artifact.get_source_map()?.ok()?,
567                artifact
568                    .get_bytecode()
569                    .and_then(|bytecode| dummy_link_bytecode(bytecode.into_owned()))?,
570            ),
571            deployed: BytecodeData::new(
572                artifact.get_source_map_deployed()?.ok()?,
573                artifact
574                    .get_deployed_bytecode()
575                    .and_then(|bytecode| dummy_link_deployed_bytecode(bytecode.into_owned()))?,
576            ),
577            function_selectors,
578            has_receive,
579            fallback_payable,
580        })
581    }
582}
583
584pub struct BytecodeData {
585    source_map: SourceMap,
586    bytecode: Bytes,
587    /// The instruction counter to program counter mapping.
588    ///
589    /// The source maps are indexed by *instruction counters*, which are the indexes of
590    /// instructions in the bytecode *minus any push bytes*.
591    ///
592    /// Since our line coverage inspector collects hit data using program counters, the anchors
593    /// also need to be based on program counters.
594    ic_pc_map: IcPcMap,
595}
596
597impl BytecodeData {
598    fn new(source_map: SourceMap, bytecode: Bytes) -> Self {
599        let ic_pc_map = IcPcMap::new(&bytecode);
600        Self { source_map, bytecode, ic_pc_map }
601    }
602
603    pub fn find_anchors(&self, source_analysis: &SourceAnalysis) -> ItemAnchors {
604        find_anchors(&self.bytecode, &self.source_map, &self.ic_pc_map, source_analysis)
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn lcov_version() {
614        assert_eq!(parse_lcov_version("0").unwrap(), Version::new(0, 0, 0));
615        assert_eq!(parse_lcov_version("1").unwrap(), Version::new(1, 0, 0));
616        assert_eq!(parse_lcov_version("1.0").unwrap(), Version::new(1, 0, 0));
617        assert_eq!(parse_lcov_version("1.1").unwrap(), Version::new(1, 1, 0));
618        assert_eq!(parse_lcov_version("1.11").unwrap(), Version::new(1, 11, 0));
619    }
620
621    #[test]
622    fn resolve_lcov_version_uses_config_when_cli_absent() {
623        let mut args = CoverageArgs::parse_from(["coverage"]);
624        let config = CoverageConfig { lcov_version: Version::new(2, 2, 0), ..Default::default() };
625
626        args.resolve_with(&config);
627
628        assert_eq!(args.lcov_version, Version::new(2, 2, 0));
629    }
630
631    #[test]
632    fn resolve_lcov_version_keeps_explicit_cli_default() {
633        let mut args = CoverageArgs::parse_from(["coverage", "--lcov-version", "1"]);
634        let config = CoverageConfig { lcov_version: Version::new(2, 2, 0), ..Default::default() };
635
636        args.resolve_with(&config);
637
638        assert_eq!(args.lcov_version, Version::new(1, 0, 0));
639    }
640
641    #[test]
642    fn resolve_lcov_version_keeps_explicit_cli_value() {
643        let mut args = CoverageArgs::parse_from(["coverage", "--lcov-version", "2"]);
644        let config = CoverageConfig { lcov_version: Version::new(2, 2, 0), ..Default::default() };
645
646        args.resolve_with(&config);
647
648        assert_eq!(args.lcov_version, Version::new(2, 0, 0));
649    }
650}