PDL: add regression tests for ‘pdl’ output

These new tests compare the output of ‘pdl’ with the output of
‘bluetooth_packetgen’ and ensures that we match this.

We will later decouple the two generators, but for now, it’s useful to
generate identical output.

Bug: 228307941
Test: atest pdl_tests
(cherry picked from https://android-review.googlesource.com/q/commit:0cb1ca8ce1623b7239759a87f94f147562c86b99)
Merged-In: Ie45ddd9fceeeefa9ad25c649bfc4db903f9fe1c2
Change-Id: Ie45ddd9fceeeefa9ad25c649bfc4db903f9fe1c2
diff --git a/tools/pdl/Android.bp b/tools/pdl/Android.bp
index 3447e21..26b0cd7 100644
--- a/tools/pdl/Android.bp
+++ b/tools/pdl/Android.bp
@@ -48,3 +48,25 @@
         ":rustfmt",
     ],
 }
+
+rust_test_host {
+    name: "pdl_tests",
+    srcs: ["test/pdl_tests.rs"],
+    test_suites: ["general-tests"],
+    enabled: false, // rustfmt is only available on x86.
+    arch: {
+        x86_64: {
+            enabled: true,
+        },
+    },
+    // LINT.IfChange
+    rustlibs: [
+        "libtempfile",
+    ],
+    // LINT.ThenChange(Cargo.toml)
+    data: [
+        ":bluetooth_packetgen",
+        ":pdl",
+        ":rustfmt",
+    ],
+}
diff --git a/tools/pdl/Cargo.toml b/tools/pdl/Cargo.toml
index d44826c..bb3429d 100644
--- a/tools/pdl/Cargo.toml
+++ b/tools/pdl/Cargo.toml
@@ -16,4 +16,6 @@
 serde_json = "*"
 structopt = "*"
 syn = "*"
+
+[dev-dependencies]
 tempfile = "*"
diff --git a/tools/pdl/src/generator.rs b/tools/pdl/src/generator.rs
index 51e7cba..1232c52 100644
--- a/tools/pdl/src/generator.rs
+++ b/tools/pdl/src/generator.rs
@@ -328,11 +328,22 @@
             writer
         })
         .collect::<Result<Vec<_>>>()?;
+
     let total_field_size = syn::Index::from(fields.iter().map(get_field_size).sum::<usize>());
+    let get_size_adjustment = (total_field_size.index > 0).then(|| {
+        Some(quote! {
+            let ret = ret + #total_field_size;
+        })
+    });
 
     code.push_str(&quote_block! {
         impl #data_name {
             fn conforms(bytes: &[u8]) -> bool {
+                // TODO(mgeisler): return Boolean expression directly.
+                // TODO(mgeisler): skip when total_field_size == 0.
+                if bytes.len() < #total_field_size {
+                    return false;
+                }
                 true
             }
 
@@ -351,7 +362,7 @@
 
             fn get_size(&self) -> usize {
                 let ret = 0;
-                let ret = ret + #total_field_size;
+                #get_size_adjustment
                 ret
             }
         }
@@ -490,7 +501,19 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_utils::{assert_eq_with_diff, parse_str, rustfmt};
+    use crate::ast;
+    use crate::parser::parse_inline;
+    use crate::test_utils::{assert_eq_with_diff, rustfmt};
+
+    /// Parse a string fragment as a PDL file.
+    ///
+    /// # Panics
+    ///
+    /// Panics on parse errors.
+    pub fn parse_str(text: &str) -> ast::Grammar {
+        let mut db = ast::SourceDatabase::new();
+        parse_inline(&mut db, String::from("stdin"), String::from(text)).expect("parse error")
+    }
 
     #[test]
     fn test_generate_preamble() {
diff --git a/tools/pdl/src/test_utils.rs b/tools/pdl/src/test_utils.rs
index bd80a61..0c05d35 100644
--- a/tools/pdl/src/test_utils.rs
+++ b/tools/pdl/src/test_utils.rs
@@ -1,14 +1,17 @@
 //! Various utility functions used in tests.
 
-use crate::ast;
-use crate::parser::parse_inline;
+// This file is included directly into integration tests in the
+// `test/` directory. These tests are compiled without access to the
+// rest of the `pdl` crate. To make this work, avoid `use crate::`
+// statements below.
+
 use std::io::Write;
 use std::process::{Command, Stdio};
 use tempfile::NamedTempFile;
 
 /// Search for a binary in `$PATH` or as a sibling to the current
 /// executable (typically the test binary).
-fn find_binary(name: &str) -> Result<std::path::PathBuf, String> {
+pub fn find_binary(name: &str) -> Result<std::path::PathBuf, String> {
     let mut current_exe = std::env::current_exe().unwrap();
     current_exe.pop();
     let paths = std::env::var_os("PATH").unwrap();
@@ -27,16 +30,6 @@
     ))
 }
 
-/// Parse a string fragment as a PDL file.
-///
-/// # Panics
-///
-/// Panics on parse errors.
-pub fn parse_str(text: &str) -> ast::Grammar {
-    let mut db = ast::SourceDatabase::new();
-    parse_inline(&mut db, String::from("stdin"), String::from(text)).expect("parse error")
-}
-
 /// Run `input` through `rustfmt`.
 ///
 /// # Panics
diff --git a/tools/pdl/test/generated/packet_decl_empty.rs b/tools/pdl/test/generated/packet_decl_empty.rs
index f4a8dcf..8205d01 100644
--- a/tools/pdl/test/generated/packet_decl_empty.rs
+++ b/tools/pdl/test/generated/packet_decl_empty.rs
@@ -11,6 +11,9 @@
 
 impl FooData {
     fn conforms(bytes: &[u8]) -> bool {
+        if bytes.len() < 0 {
+            return false;
+        }
         true
     }
     fn parse(bytes: &[u8]) -> Result<Self> {
@@ -22,7 +25,6 @@
     }
     fn get_size(&self) -> usize {
         let ret = 0;
-        let ret = ret + 0;
         ret
     }
 }
diff --git a/tools/pdl/test/generated/packet_decl_simple_big_endian.rs b/tools/pdl/test/generated/packet_decl_simple_big_endian.rs
index 89b1bab..8830d77 100644
--- a/tools/pdl/test/generated/packet_decl_simple_big_endian.rs
+++ b/tools/pdl/test/generated/packet_decl_simple_big_endian.rs
@@ -17,6 +17,9 @@
 
 impl FooData {
     fn conforms(bytes: &[u8]) -> bool {
+        if bytes.len() < 3 {
+            return false;
+        }
         true
     }
     fn parse(bytes: &[u8]) -> Result<Self> {
diff --git a/tools/pdl/test/generated/packet_decl_simple_little_endian.rs b/tools/pdl/test/generated/packet_decl_simple_little_endian.rs
index 5b81ec8..5dffc6b 100644
--- a/tools/pdl/test/generated/packet_decl_simple_little_endian.rs
+++ b/tools/pdl/test/generated/packet_decl_simple_little_endian.rs
@@ -17,6 +17,9 @@
 
 impl FooData {
     fn conforms(bytes: &[u8]) -> bool {
+        if bytes.len() < 3 {
+            return false;
+        }
         true
     }
     fn parse(bytes: &[u8]) -> Result<Self> {
diff --git a/tools/pdl/test/pdl_tests.rs b/tools/pdl/test/pdl_tests.rs
new file mode 100644
index 0000000..89b3b92
--- /dev/null
+++ b/tools/pdl/test/pdl_tests.rs
@@ -0,0 +1,98 @@
+use std::fs;
+use std::process::Command;
+
+// The integration test in this file is not part of the pdl crate, and
+// so we cannot directly depend on anything from pdl. However, we can
+// include the test_utils.rs file directly.
+
+#[path = "../src/test_utils.rs"]
+mod test_utils;
+use test_utils::{assert_eq_with_diff, find_binary, rustfmt};
+
+fn strip_blank_lines(text: &str) -> String {
+    text.lines().filter(|line| !line.trim().is_empty()).collect::<Vec<_>>().join("\n")
+}
+
+/// Run `code` through `pdl`.
+///
+/// # Panics
+///
+/// Panics if `pdl` cannot be found on `$PATH` or if it returns a
+/// non-zero exit code.
+fn pdl(code: &str) -> String {
+    let tempdir = tempfile::tempdir().unwrap();
+    let input = tempdir.path().join("input.pdl");
+    fs::write(&input, code.as_bytes()).unwrap();
+    let pdl_path = find_binary("pdl").unwrap();
+    let output = Command::new(&pdl_path)
+        .arg("--output-format")
+        .arg("rust")
+        .arg(input)
+        .output()
+        .expect("pdl failed");
+    assert!(output.status.success(), "pdl failure: {:?}, input:\n{}", output, code);
+    String::from_utf8(output.stdout).unwrap()
+}
+
+/// Run `code` through `bluetooth_packetgen`.
+///
+/// # Panics
+///
+/// Panics if `bluetooth_packetgen` cannot be found on `$PATH` or if
+/// it returns a non-zero exit code.
+fn bluetooth_packetgen(code: &str) -> String {
+    let tempdir = tempfile::tempdir().unwrap();
+    let tempdir_path = tempdir.path().to_str().unwrap();
+    let input_path = tempdir.path().join("input.pdl");
+    let output_path = input_path.with_extension("rs");
+    fs::write(&input_path, code.as_bytes()).unwrap();
+    let bluetooth_packetgen_path = find_binary("bluetooth_packetgen").unwrap();
+    let output = Command::new(&bluetooth_packetgen_path)
+        .arg(&format!("--include={}", tempdir_path))
+        .arg(&format!("--out={}", tempdir_path))
+        .arg("--rust")
+        .arg(input_path)
+        .output()
+        .expect("bluetooth_packetgen failed");
+    assert!(output.status.success(), "bluetooth_packetgen failure: {:?}, input:\n{}", output, code);
+    fs::read_to_string(output_path).unwrap()
+}
+
+#[track_caller]
+fn assert_equal_compilation(pdl_code: &str) {
+    let old_rust = rustfmt(&bluetooth_packetgen(pdl_code));
+    let new_rust = rustfmt(&pdl(pdl_code));
+    assert_eq_with_diff(&strip_blank_lines(&old_rust), &strip_blank_lines(&new_rust));
+}
+
+#[test]
+fn test_prelude() {
+    let pdl_code = r#"
+        little_endian_packets
+    "#;
+    assert_equal_compilation(pdl_code);
+}
+
+#[test]
+fn test_empty_packet() {
+    let pdl_code = r#"
+        little_endian_packets
+
+        packet Foo {
+        }
+    "#;
+    assert_equal_compilation(pdl_code);
+}
+
+#[test]
+fn test_simple_le_packet() {
+    let pdl_code = r#"
+        little_endian_packets
+
+        packet Foo {
+          a: 8,
+          b: 16,
+        }
+    "#;
+    assert_equal_compilation(pdl_code);
+}