cxx_qt_gen/
lib.rs

1// SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3// SPDX-FileContributor: Gerhard de Clercq <gerhard.declercq@kdab.com>
4//
5// SPDX-License-Identifier: MIT OR Apache-2.0
6
7#![deny(missing_docs)]
8
9//! The cxx-qt-gen crate provides methods for generated C++ and Rust code from a TokenStream.
10
11mod generator;
12mod naming;
13mod parser;
14mod syntax;
15mod writer;
16
17pub use generator::{
18    cpp::{fragment::CppFragment, GeneratedCppBlocks},
19    rust::GeneratedRustBlocks,
20};
21pub use parser::Parser;
22pub use syntax::{parse_qt_file, CxxQtFile, CxxQtItem};
23pub use writer::{cpp::write_cpp, rust::write_rust};
24
25pub use syn::{Error, Result};
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30
31    use crate::generator::cpp::property::tests::require_pair;
32    use clang_format::{clang_format_with_style, ClangFormatStyle};
33    use generator::{cpp::GeneratedCppBlocks, rust::GeneratedRustBlocks};
34    use parser::Parser;
35    use pretty_assertions::assert_str_eq;
36    use proc_macro2::TokenStream;
37    use quote::{quote, ToTokens};
38    use std::{
39        env,
40        fs::OpenOptions,
41        io::Write,
42        path::{Path, PathBuf},
43    };
44    use writer::{cpp::write_cpp, rust::write_rust};
45
46    /// Helper to ensure that a given syn item is the same as the given TokenStream
47    pub fn assert_tokens_eq<T: ToTokens>(item: &T, tokens: TokenStream) {
48        // For understanding what's going on, it is nicer to use format_rs_source
49        // So that the TokenStream is actually legible.
50        //
51        // We don't want to use format_rs_source for the comparison, because it means calling out
52        // to rustfmt, which is slow.
53        // So only pretty-print if the comparison fails.
54        let left = tokens.to_string();
55        let right = item.to_token_stream().to_string();
56        if left != right {
57            assert_str_eq!(format_rs_source(&left), format_rs_source(&right));
58            // Fallback, in case assert_str_eq doesn't actually panic after formatting for some
59            // reason.
60            // CODECOV_EXCLUDE_START
61            assert_str_eq!(left, right);
62            // CODECOV_EXCLUDE_STOP
63        }
64    }
65
66    macro_rules! assert_parse_errors {
67        { $parse_fn:expr => $($input:tt)* } => {
68            $(assert!($parse_fn(syn::parse_quote! $input).is_err());)*
69        }
70    }
71    pub(crate) use assert_parse_errors;
72
73    /// Helper for formating C++ code
74    pub(crate) fn format_cpp(cpp_code: &str) -> String {
75        clang_format_with_style(cpp_code, &ClangFormatStyle::File).unwrap()
76    }
77
78    /// Helper for format Rust code
79    fn format_rs_source(rs_code: &str) -> String {
80        // NOTE: this error handling is pretty rough so should only used for tests
81        let mut command = std::process::Command::new("rustfmt");
82        let mut child = command
83            .args(["--emit", "stdout"])
84            .stdin(std::process::Stdio::piped())
85            .stdout(std::process::Stdio::piped())
86            .spawn()
87            .unwrap();
88
89        // Scope stdin to force an automatic flush
90        {
91            let mut stdin = child.stdin.take().unwrap();
92            write!(stdin, "{rs_code}").unwrap();
93        }
94
95        let output = child.wait_with_output().unwrap();
96        let output = String::from_utf8(output.stdout).unwrap();
97
98        // Quote does not retain empty lines so we throw them away in the case of the
99        // reference string as to not cause clashes
100        output.replace("\n\n", "\n")
101    }
102
103    fn sanitize_code(mut code: String) -> String {
104        code.retain(|c| c != '\r');
105        code
106    }
107
108    // CODECOV_EXCLUDE_START
109    fn update_expected_file(path: PathBuf, source: &str) {
110        println!("Updating expected file: {path:?}");
111
112        let mut file = OpenOptions::new()
113            .write(true)
114            .truncate(true)
115            .open(path)
116            .unwrap();
117        file.write_all(source.as_bytes()).unwrap();
118    }
119    // CODECOV_EXCLUDE_STOP
120
121    fn update_expected(test_name: &str, rust: &str, header: &str, source: &str) -> bool {
122        // Ideally we'd be able to get the path from `file!()`, but that unfortunately only
123        // gives us a relative path, which isn't really all that useful.
124        // So require the path of the crate to be set via an environment variable.
125        //
126        // In the simplest case this can be achieved by running:
127        //
128        //      CXX_QT_UPDATE_EXPECTED=$(pwd) cargo test
129        //
130        if let Ok(path) = env::var("CXX_QT_UPDATE_EXPECTED") {
131            // CODECOV_EXCLUDE_START
132            let output_folder = Path::new(&path);
133            let output_folder = output_folder.join("test_outputs");
134
135            let update = |file_ending, contents| {
136                update_expected_file(
137                    output_folder.join(format!("{test_name}.{file_ending}")),
138                    contents,
139                );
140            };
141            update("rs", rust);
142            update("h", header);
143            update("cpp", source);
144
145            true
146            // CODECOV_EXCLUDE_STOP
147        } else {
148            false
149        }
150    }
151
152    fn test_code_generation_internal(
153        test_name: &str,
154        input: &str,
155        expected_rust_output: &str,
156        expected_cpp_header: &str,
157        expected_cpp_source: &str,
158    ) {
159        let parser = Parser::from(syn::parse_str(input).unwrap()).unwrap();
160
161        let generated_cpp = GeneratedCppBlocks::from(&parser).unwrap();
162        let (mut header, mut source) =
163            require_pair(&write_cpp(&generated_cpp, "directory/file_ident")).unwrap();
164        header = sanitize_code(header);
165        source = sanitize_code(source);
166
167        let generated_rust = GeneratedRustBlocks::from(&parser).unwrap();
168        let rust = sanitize_code(format_rs_source(
169            &write_rust(&generated_rust, Some("directory/file_ident")).to_string(),
170        ));
171
172        // CODECOV_EXCLUDE_START
173        if !update_expected(test_name, &rust, &header, &source) {
174            assert_str_eq!(sanitize_code(expected_cpp_header.to_owned()), header);
175            assert_str_eq!(sanitize_code(expected_cpp_source.to_owned()), source);
176            assert_str_eq!(sanitize_code(expected_rust_output.to_owned()), rust);
177        }
178        // CODECOV_EXCLUDE_STOP
179    }
180
181    /// Helper for testing if a given input Rust file generates the expected C++ & Rust code
182    /// This needs to be a macro rather than a function because include_str needs the file path at compile time.
183    macro_rules! test_code_generation {
184        ( $file_stem:literal ) => {
185            test_code_generation_internal(
186                $file_stem,
187                include_str!(concat!("../test_inputs/", $file_stem, ".rs")),
188                include_str!(concat!("../test_outputs/", $file_stem, ".rs")),
189                include_str!(concat!("../test_outputs/", $file_stem, ".h")),
190                include_str!(concat!("../test_outputs/", $file_stem, ".cpp")),
191            );
192        };
193    }
194
195    #[test]
196    fn generates_invokables() {
197        test_code_generation!("invokables");
198    }
199
200    #[test]
201    fn generates_passthrough_and_naming() {
202        test_code_generation!("passthrough_and_naming");
203    }
204
205    #[test]
206    fn generates_properties() {
207        test_code_generation!("properties");
208    }
209
210    #[test]
211    fn generates_signals() {
212        test_code_generation!("signals");
213    }
214
215    #[test]
216    fn generates_inheritance() {
217        test_code_generation!("inheritance");
218    }
219
220    #[test]
221    fn generates_qenum() {
222        test_code_generation!("qenum");
223    }
224
225    #[test]
226    #[should_panic]
227    fn fail_token_assert() {
228        assert_tokens_eq(
229            &quote! { struct MyStruct; },
230            quote! { struct MyOtherStruct; },
231        )
232    }
233}