nimbus_fml/backends/kotlin/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::command_line::commands::GenerateStructCmd;
6use crate::error::{FMLError, Result};
7use crate::frontend::AboutBlock;
8use crate::intermediate_representation::FeatureManifest;
9use askama::Template;
10
11mod gen_structs;
12
13impl AboutBlock {
14    fn nimbus_fully_qualified_name(&self) -> String {
15        let kt_about = self.kotlin_about.as_ref().unwrap();
16
17        let class = &kt_about.class;
18        if class.starts_with('.') {
19            format!("{}{}", kt_about.package, class)
20        } else {
21            class.clone()
22        }
23    }
24
25    fn nimbus_object_name_kt(&self) -> String {
26        let fqe = self.nimbus_fully_qualified_name();
27        let last = fqe.split('.').next_back().unwrap_or(&fqe);
28        last.to_string()
29    }
30
31    fn nimbus_package_name(&self) -> Option<String> {
32        let fqe = self.nimbus_fully_qualified_name();
33        if !fqe.contains('.') {
34            return None;
35        }
36        let mut it = fqe.split('.');
37        it.next_back()?;
38        Some(it.collect::<Vec<&str>>().join("."))
39    }
40
41    fn resource_package_name(&self) -> String {
42        let kt_about = self.kotlin_about.as_ref().unwrap();
43        kt_about.package.clone()
44    }
45}
46
47pub(crate) fn generate_struct(manifest: &FeatureManifest, cmd: &GenerateStructCmd) -> Result<()> {
48    if manifest.about.kotlin_about.is_none() {
49        return Err(FMLError::ValidationError(
50            "about".to_string(),
51            format!(
52                "The `about` block is missing a valid `android` or `kotlin` entry: {}",
53                &cmd.manifest
54            ),
55        ));
56    }
57
58    let path = &cmd.output;
59    let path = if path.is_dir() {
60        path.join(format!("{}.kt", manifest.about.nimbus_object_name_kt()))
61    } else {
62        path.clone()
63    };
64
65    let kt = gen_structs::FeatureManifestDeclaration::new(manifest);
66
67    let contents = kt.render()?;
68
69    std::fs::write(path, contents)?;
70
71    Ok(())
72}
73
74#[cfg(test)]
75pub mod test {
76    use crate::util::{join, pkg_dir, sdk_dir};
77    use anyhow::{bail, Result};
78    use std::path::Path;
79    use std::process::Command;
80
81    // The root of the Android kotlin package structure
82    fn sdk_android_dir() -> String {
83        join(sdk_dir(), "android/src/main/java")
84    }
85
86    // The directory with the mock implementations of Android
87    // used for testing.
88    fn runtime_dir() -> String {
89        join(pkg_dir(), "fixtures/android/runtime")
90    }
91
92    // We'll put our test scripts in here.
93    fn tests_dir() -> String {
94        join(pkg_dir(), "fixtures/android/tests")
95    }
96
97    // The jar archive we need to do JSON with in Kotlin/Java.
98    // This is the same library as bundled in Android.
99    fn json_jar() -> String {
100        join(runtime_dir(), "json.jar")
101    }
102
103    // The file with the kt implementation of FeatureVariables
104    fn variables_kt() -> String {
105        join(
106            sdk_android_dir(),
107            "org/mozilla/experiments/nimbus/FeatureVariables.kt",
108        )
109    }
110
111    fn nimbus_internals_kt() -> String {
112        join(sdk_android_dir(), "org/mozilla/experiments/nimbus/internal")
113    }
114
115    // The file with the kt implementation of FeatureVariables
116    fn features_kt() -> String {
117        join(
118            sdk_android_dir(),
119            "org/mozilla/experiments/nimbus/FeaturesInterface.kt",
120        )
121    }
122
123    fn hardcoded_features_kt() -> String {
124        join(
125            sdk_android_dir(),
126            "org/mozilla/experiments/nimbus/HardcodedNimbusFeatures.kt",
127        )
128    }
129
130    fn classpath(classes: &Path) -> Result<String> {
131        Ok(format!("{}:{}", json_jar(), classes.to_str().unwrap()))
132    }
133
134    fn detect_kotlinc() -> Result<bool> {
135        let output = Command::new("which").arg("kotlinc").output()?;
136
137        Ok(output.status.success())
138    }
139
140    // Compile a genertaed manifest file against the mocked out Android runtime.
141    pub fn compile_manifest_kt(manifest_paths: &[String]) -> Result<tempfile::TempDir> {
142        let temp = tempfile::tempdir()?;
143        let build_dir = temp.path();
144
145        let status = Command::new("kotlinc")
146            // Our generated bindings should not produce any warnings; fail tests if they do.
147            .arg("-Werror")
148            .arg("-J-ea")
149            // Reflect $CLASSPATH from the environment, to help find `json.jar`.
150            .arg("-classpath")
151            .arg(json_jar())
152            .arg("-d")
153            .arg(build_dir)
154            .arg(variables_kt())
155            .arg(features_kt())
156            .arg(hardcoded_features_kt())
157            .arg(runtime_dir())
158            .arg(nimbus_internals_kt())
159            .args(manifest_paths)
160            .spawn()?
161            .wait()?;
162        if status.success() {
163            Ok(temp)
164        } else {
165            bail!("running `kotlinc` failed compiling a generated manifest")
166        }
167    }
168
169    // Given a generated manifest, run a kts script against it.
170    pub fn run_script_with_generated_code(manifests_kt: &[String], script: &str) -> Result<()> {
171        if !detect_kotlinc()? {
172            println!("SDK-446 Install kotlinc or add it the PATH to run tests");
173            return Ok(());
174        }
175        let temp_dir = compile_manifest_kt(manifests_kt)?;
176        let build_dir = temp_dir.path();
177
178        let status = Command::new("kotlinc")
179            // Our generated bindings should not produce any warnings; fail tests if they do.
180            .arg("-Werror")
181            .arg("-J-ea")
182            // Reflect $CLASSPATH from the environment, to help find `json.jar`.
183            .arg("-classpath")
184            .arg(&classpath(build_dir)?)
185            .arg("-script")
186            .arg(script)
187            .spawn()?
188            .wait()?;
189
190        drop(temp_dir);
191        if status.success() {
192            Ok(())
193        } else {
194            bail!("running `kotlinc` failed running a script")
195        }
196    }
197
198    #[test]
199    fn smoke_test_runtime_dir() -> Result<()> {
200        run_script_with_generated_code(
201            &[join(tests_dir(), "SmokeTestFeature.kt")],
202            "fixtures/android/tests/smoke_test.kts",
203        )?;
204        Ok(())
205    }
206}