1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//! This file contains "golden" tests, which compare the output of known sample
//! `Cargo.toml` files with known fixed reference output files.
//!
//! TODO(https://fxbug.dev/96111) move these golden specs into GN

use {
    anyhow::Context,
    argh::FromArgs,
    // Without this, the test diffs are impractical to debug.
    pretty_assertions::assert_eq,
    std::fmt::{Debug, Display},
    std::path::{Path, PathBuf},
    tempfile,
};

#[derive(FromArgs, Debug)]
/// Paths to use in test. All paths are relative to where this test is executed.
///
/// These paths have to be relative when passed to this test on infra bots, so they are mapped
/// correctly, otherwise they won't be available at test runtime. It is safe to convert these to
/// absolute paths later in the test.
struct Paths {
    /// path to the directory where golden tests are placed.
    #[argh(option)]
    test_base_dir: String,
    /// path to `rustc` binary to use in test.
    #[argh(option)]
    rustc_binary_path: String,
    /// path to `gn` binary to use in test.
    #[argh(option)]
    gn_binary_path: String,
    /// path to `cargo` binary to use in test.
    #[argh(option)]
    cargo_binary_path: String,
    /// path to shared libraries directory to use in test.
    #[argh(option)]
    lib_path: String,
}

#[derive(PartialEq, Eq)]
struct DisplayAsDebug<T: Display>(T);

impl<T: Display> Debug for DisplayAsDebug<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Display::fmt(&self.0, f)
    }
}

fn main() {
    let paths: Paths = argh::from_env();
    eprintln!("paths: {:?}", &paths);

    // Shared library setup for Linux and Mac.  Systems will ignore the settings
    // that don't apply to them.
    //
    // These values need to be absolute so they work regardless of the current working directory.
    std::env::set_var("LD_LIBRARY_PATH", Path::new(&paths.lib_path).canonicalize().unwrap());
    std::env::set_var("DYLD_LIBRARY_PATH", Path::new(&paths.lib_path).canonicalize().unwrap());

    // Cargo internally invokes rustc; but we must tell it to use the one from
    // our sandbox, and this is configured using the env variable "RUSTC".
    //
    // This value needs to be absolute so it works regardless of the current working directory.
    //
    // See:
    // https://doc.rust-lang.org/cargo/reference/environment-variables.html
    std::env::set_var("RUSTC", Path::new(&paths.rustc_binary_path).canonicalize().unwrap());

    #[derive(Debug)]
    struct TestCase {
        /// Manifest file path (`Cargo.toml`); relative to the base test directory.
        manifest_path: Vec<&'static str>,
        /// Expected file (`BUILD.gn`); relative to the base test directory.
        golden_expected_filename: Vec<&'static str>,
        /// Extra arguments to pass to gnaw.
        extra_args: Vec<&'static str>,
    }

    let tests = vec![
        TestCase {
            manifest_path: vec!["simple", "Cargo.toml"],
            golden_expected_filename: vec!["simple", "BUILD.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["simple_deps", "Cargo.toml"],
            golden_expected_filename: vec!["simple_deps", "BUILD.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["simple_deps", "Cargo.toml"],
            golden_expected_filename: vec!["simple_deps", "BUILD_WITH_NO_ROOT.gn"],
            extra_args: vec!["--skip-root"],
        },
        TestCase {
            manifest_path: vec!["platform_deps", "Cargo.toml"],
            golden_expected_filename: vec!["platform_deps", "BUILD.gn"],
            extra_args: vec!["--skip-root"],
        },
        TestCase {
            manifest_path: vec!["platform_features", "Cargo.toml"],
            golden_expected_filename: vec!["platform_features", "BUILD.gn"],
            extra_args: vec!["--skip-root"],
        },
        TestCase {
            manifest_path: vec!["binary", "Cargo.toml"],
            golden_expected_filename: vec!["binary", "BUILD.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["binary_with_tests", "Cargo.toml"],
            golden_expected_filename: vec!["binary_with_tests", "BUILD.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["multiple_crate_types", "Cargo.toml"],
            golden_expected_filename: vec!["multiple_crate_types", "BUILD.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["feature_review", "Cargo.toml"],
            golden_expected_filename: vec!["feature_review", "BUILD.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["cargo_features", "Cargo.toml"],
            golden_expected_filename: vec!["cargo_features", "BUILD-default.gn"],
            extra_args: vec![],
        },
        TestCase {
            manifest_path: vec!["cargo_features", "Cargo.toml"],
            golden_expected_filename: vec!["cargo_features", "BUILD-all-features.gn"],
            extra_args: vec!["--all-features"],
        },
        TestCase {
            manifest_path: vec!["cargo_features", "Cargo.toml"],
            golden_expected_filename: vec!["cargo_features", "BUILD-no-default-features.gn"],
            extra_args: vec!["--no-default-features"],
        },
        TestCase {
            manifest_path: vec!["cargo_features", "Cargo.toml"],
            golden_expected_filename: vec!["cargo_features", "BUILD-featurefoo.gn"],
            extra_args: vec!["--features", "featurefoo"],
        },
        TestCase {
            manifest_path: vec!["visibility", "Cargo.toml"],
            golden_expected_filename: vec!["visibility", "BUILD.gn"],
            extra_args: vec!["--skip-root"],
        },
        TestCase {
            manifest_path: vec!["target_renaming", "Cargo.toml"],
            golden_expected_filename: vec!["target_renaming", "BUILD.gn"],
            extra_args: vec!["--skip-root"],
        },
    ];

    let run_gnaw = |manifest_path: &[&str], extra_args: &[&str]| {
        let test_dir = tempfile::TempDir::new().unwrap();
        let mut manifest_path: PathBuf =
            test_dir.path().join(manifest_path.iter().collect::<PathBuf>());
        let output = test_dir.path().join("BUILD.gn");

        // we need the emitted file to be under the same path as the gn targets it references
        let test_base_dir = PathBuf::from(&paths.test_base_dir);
        copy_contents(&test_base_dir, test_dir.path());

        if manifest_path.file_name().unwrap() != "Cargo.toml" {
            // rename manifest so that `cargo metadata` is happy.
            let manifest_dest_path =
                manifest_path.parent().expect("getting Cargo.toml parent dir").join("Cargo.toml");
            std::fs::copy(&manifest_path, &manifest_dest_path).expect("writing Cargo.toml");
            manifest_path = manifest_dest_path;
        }

        let project_root = test_dir.path().to_str().unwrap().to_owned();
        // Note: argh does not support "--flag=value" or "--bool-flag false".
        let absolute_cargo_binary_path =
            Path::new(&paths.cargo_binary_path).canonicalize().unwrap();
        let mut args: Vec<&str> = vec![
            // args[0] is not used in arg parsing, so this can be any string.
            "fake_binary_name",
            "--manifest-path",
            manifest_path.to_str().unwrap(),
            "--project-root",
            &project_root,
            "--output",
            output.to_str().unwrap(),
            "--gn-bin",
            &paths.gn_binary_path,
            "--cargo",
            // Cargo is not executed in another working directory by gnaw_lib, so an absolute path
            // is necessary here.
            absolute_cargo_binary_path.to_str().unwrap(),
        ];
        args.extend(extra_args);
        gnaw_lib::run(&args)
            .with_context(|| format!("error running gnaw with args: {:?}\n\t", &args))?;
        let output = std::fs::read_to_string(&output)
            .with_context(|| format!("while reading tempfile: {}", output.display()))
            .expect("tempfile read success");
        Result::<_, anyhow::Error>::Ok(output)
    };

    for test in tests {
        let output = run_gnaw(&test.manifest_path, &test.extra_args)
            .with_context(|| format!("\n\ttest was: {:?}", &test))
            .expect("gnaw_lib::run should succeed");

        let test_base_dir = PathBuf::from(&paths.test_base_dir);
        let expected_path: PathBuf =
            test_base_dir.join(test.golden_expected_filename.iter().collect::<PathBuf>());
        let expected = std::fs::read_to_string(expected_path.to_string_lossy().to_string())
            .with_context(|| {
                format!("while reading expected: {:?}", &test.golden_expected_filename)
            })
            .expect("expected file read success");
        assert_eq!(
            DisplayAsDebug(&expected),
            DisplayAsDebug(&output),
            "left: expected; right: actual: {:?}\n\nGenerated content:\n----------\n{}\n----------\n",
            &test,
            &output
        );
    }

    #[derive(Debug)]
    struct ExpectFailCase {
        /// Manifest file path (`Cargo.toml`); relative to the base test directory.
        manifest_path: Vec<&'static str>,
        /// Expected string to search for in returned error.
        expected_error_substring: &'static str,
        /// Extra arguments to pass to gnaw.
        extra_args: Vec<&'static str>,
    }
    let tests = vec![
        ExpectFailCase {
            manifest_path: vec!["feature_review", "Cargo_unreviewed_feature.toml"],
            expected_error_substring:
                "crate_with_features 0.1.0 is included with unreviewed features [\"feature1\"]",
            extra_args: vec![],
        },
        ExpectFailCase {
            manifest_path: vec!["feature_review", "Cargo_missing_review.toml"],
            expected_error_substring:
                "crate_with_features 0.1.0 requires feature review but reviewed features not found",
            extra_args: vec![],
        },
        ExpectFailCase {
            manifest_path: vec!["feature_review", "Cargo_extra_review.toml"],
            expected_error_substring:
                "crate_with_features 0.1.0 sets reviewed_features but crate_with_features was not found in require_feature_reviews",
            extra_args: vec![],
        },
    ];
    for test in tests {
        let result = run_gnaw(&test.manifest_path, &test.extra_args);
        let error = match result {
            Ok(_) => panic!("gnaw unexpectedly succeeded for {:?}", test),
            Err(e) => e,
        };
        if error.chain().find(|e| e.to_string().contains(test.expected_error_substring)).is_none() {
            panic!(
                "expected error to contain {:?}, was: {:?}",
                test.expected_error_substring, error
            );
        }
    }
}

fn copy_contents(original_test_dir: &Path, test_dir_path: &Path) {
    // copy the contents of original test dir to test_dir
    for entry in walkdir::WalkDir::new(&original_test_dir) {
        let entry = entry.expect("walking original test directory to copy files to /tmp");
        if !entry.file_type().is_file() {
            continue;
        }
        let to_copy = entry.path();
        let destination = test_dir_path.join(to_copy.strip_prefix(&original_test_dir).unwrap());
        std::fs::create_dir_all(destination.parent().unwrap())
            .expect("making parent of file to copy");
        std::fs::copy(to_copy, destination).expect("copying file");
    }
    println!("done copying files");
}