| 1 | // Copyright 2025, Linaro Limited |
| 2 | // Author(s): Manos Pitsidianakis <manos.pitsidianakis@linaro.org> |
| 3 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 4 | |
| 5 | use quote::quote; |
| 6 | |
| 7 | use super::*; |
| 8 | |
| 9 | macro_rules! derive_compile_fail { |
| 10 | ($derive_fn:path, $input:expr, $($error_msg:expr),+ $(,)?) => {{ |
| 11 | let input: proc_macro2::TokenStream = $input; |
| 12 | let error_msg = &[$( quote! { ::core::compile_error! { $error_msg } } ),*]; |
| 13 | let derive_fn: fn(input: syn::DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> = |
| 14 | $derive_fn; |
| 15 | |
| 16 | let input: syn::DeriveInput = syn::parse2(input).unwrap(); |
| 17 | let result = derive_fn(input); |
| 18 | let err = result.unwrap_err().into_compile_error(); |
| 19 | assert_eq!( |
| 20 | err.to_string(), |
| 21 | quote! { #(#error_msg)* }.to_string() |
| 22 | ); |
| 23 | }}; |
| 24 | } |
| 25 | |
| 26 | macro_rules! derive_compile { |
| 27 | ($derive_fn:path, $input:expr, $($expected:tt)*) => {{ |
| 28 | let input: proc_macro2::TokenStream = $input; |
| 29 | let expected: proc_macro2::TokenStream = $($expected)*; |
| 30 | let derive_fn: fn(input: syn::DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> = |
| 31 | $derive_fn; |
| 32 | |
| 33 | let input: syn::DeriveInput = syn::parse2(input).unwrap(); |
| 34 | let result = derive_fn(input).unwrap(); |
| 35 | assert_eq!(result.to_string(), expected.to_string()); |
| 36 | }}; |
| 37 | } |
| 38 | |
| 39 | #[test] |
| 40 | fn test_derive_device() { |
| 41 | // Check that repr(C) is used |
| 42 | derive_compile_fail!( |
| 43 | derive_device_or_error, |
| 44 | quote! { |
| 45 | #[derive(Device)] |
| 46 | struct Foo { |
| 47 | _unused: [u8; 0], |
| 48 | } |
| 49 | }, |
| 50 | "#[repr(C)] required for #[derive(Device)]" |
| 51 | ); |
| 52 | // Check that invalid/misspelled attributes raise an error |
| 53 | derive_compile_fail!( |
| 54 | derive_device_or_error, |
| 55 | quote! { |
| 56 | #[repr(C)] |
| 57 | #[derive(Device)] |
| 58 | struct DummyState { |
| 59 | #[property(defalt = true)] |
| 60 | migrate_clock: bool, |
| 61 | } |
| 62 | }, |
| 63 | "Expected one of `bit`, `default` or `rename`" |
| 64 | ); |
| 65 | // Check that repeated attributes are not allowed: |
| 66 | derive_compile_fail!( |
| 67 | derive_device_or_error, |
| 68 | quote! { |
| 69 | #[repr(C)] |
| 70 | #[derive(Device)] |
| 71 | struct DummyState { |
| 72 | #[property(rename = "migrate-clk", rename = "migrate-clk", default = true)] |
| 73 | migrate_clock: bool, |
| 74 | } |
| 75 | }, |
| 76 | "Duplicate argument", |
| 77 | "Already used here", |
| 78 | ); |
| 79 | derive_compile_fail!( |
| 80 | derive_device_or_error, |
| 81 | quote! { |
| 82 | #[repr(C)] |
| 83 | #[derive(Device)] |
| 84 | struct DummyState { |
| 85 | #[property(default = true, default = true)] |
| 86 | migrate_clock: bool, |
| 87 | } |
| 88 | }, |
| 89 | "Duplicate argument", |
| 90 | "Already used here", |
| 91 | ); |
| 92 | derive_compile_fail!( |
| 93 | derive_device_or_error, |
| 94 | quote! { |
| 95 | #[repr(C)] |
| 96 | #[derive(Device)] |
| 97 | struct DummyState { |
| 98 | #[property(bit = 0, bit = 1)] |
| 99 | flags: u32, |
| 100 | } |
| 101 | }, |
| 102 | "Duplicate argument", |
| 103 | "Already used here", |
| 104 | ); |
| 105 | // Check that the field name is preserved when `rename` isn't used: |
| 106 | derive_compile!( |
| 107 | derive_device_or_error, |
| 108 | quote! { |
| 109 | #[repr(C)] |
| 110 | #[derive(Device)] |
| 111 | pub struct DummyState { |
| 112 | parent: ParentField<DeviceState>, |
| 113 | #[property(default = true)] |
| 114 | migrate_clock: bool, |
| 115 | } |
| 116 | }, |
| 117 | quote! { |
| 118 | unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { |
| 119 | const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ |
| 120 | ::hwcore::bindings::Property { |
| 121 | name: ::std::ffi::CStr::as_ptr(c"migrate_clock"), |
| 122 | info: <bool as ::hwcore::QDevProp>::BASE_INFO, |
| 123 | offset: ::core::mem::offset_of!(DummyState, migrate_clock) as isize, |
| 124 | bitnr: 0, |
| 125 | set_default: true, |
| 126 | defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: true as u64 }, |
| 127 | ..::common::Zeroable::ZERO |
| 128 | } |
| 129 | ]; |
| 130 | } |
| 131 | } |
| 132 | ); |
| 133 | // Check that `rename` value is used for the property name when used: |
| 134 | derive_compile!( |
| 135 | derive_device_or_error, |
| 136 | quote! { |
| 137 | #[repr(C)] |
| 138 | #[derive(Device)] |
| 139 | pub struct DummyState { |
| 140 | parent: ParentField<DeviceState>, |
| 141 | #[property(rename = "migrate-clk", default = true)] |
| 142 | migrate_clock: bool, |
| 143 | } |
| 144 | }, |
| 145 | quote! { |
| 146 | unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { |
| 147 | const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ |
| 148 | ::hwcore::bindings::Property { |
| 149 | name: ::std::ffi::CStr::as_ptr(c"migrate-clk"), |
| 150 | info: <bool as ::hwcore::QDevProp>::BASE_INFO, |
| 151 | offset: ::core::mem::offset_of!(DummyState, migrate_clock) as isize, |
| 152 | bitnr: 0, |
| 153 | set_default: true, |
| 154 | defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: true as u64 }, |
| 155 | ..::common::Zeroable::ZERO |
| 156 | } |
| 157 | ]; |
| 158 | } |
| 159 | } |
| 160 | ); |
| 161 | // Check that `bit` value is used for the bit property without default |
| 162 | // value (note: though C macro (e.g., DEFINE_PROP_BIT) always requires |
| 163 | // default value, Rust side allows to default this field to "0"): |
| 164 | derive_compile!( |
| 165 | derive_device_or_error, |
| 166 | quote! { |
| 167 | #[repr(C)] |
| 168 | #[derive(Device)] |
| 169 | pub struct DummyState { |
| 170 | parent: ParentField<DeviceState>, |
| 171 | #[property(bit = 3)] |
| 172 | flags: u32, |
| 173 | } |
| 174 | }, |
| 175 | quote! { |
| 176 | unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { |
| 177 | const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ |
| 178 | ::hwcore::bindings::Property { |
| 179 | name: ::std::ffi::CStr::as_ptr(c"flags"), |
| 180 | info: <u32 as ::hwcore::QDevProp>::BIT_INFO, |
| 181 | offset: ::core::mem::offset_of!(DummyState, flags) as isize, |
| 182 | bitnr : { |
| 183 | const { assert!(3 >= 0 && 3 < u32::BITS as _ , "bit number exceeds type bits range"); } |
| 184 | 3 as u8 |
| 185 | }, |
| 186 | set_default: false, |
| 187 | defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: 0 as u64 }, |
| 188 | ..::common::Zeroable::ZERO |
| 189 | } |
| 190 | ]; |
| 191 | } |
| 192 | } |
| 193 | ); |
| 194 | // Check that `bit` value is used for the bit property when used: |
| 195 | derive_compile!( |
| 196 | derive_device_or_error, |
| 197 | quote! { |
| 198 | #[repr(C)] |
| 199 | #[derive(Device)] |
| 200 | pub struct DummyState { |
| 201 | parent: ParentField<DeviceState>, |
| 202 | #[property(bit = 3, default = true)] |
| 203 | flags: u32, |
| 204 | } |
| 205 | }, |
| 206 | quote! { |
| 207 | unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { |
| 208 | const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ |
| 209 | ::hwcore::bindings::Property { |
| 210 | name: ::std::ffi::CStr::as_ptr(c"flags"), |
| 211 | info: <u32 as ::hwcore::QDevProp>::BIT_INFO, |
| 212 | offset: ::core::mem::offset_of!(DummyState, flags) as isize, |
| 213 | bitnr : { |
| 214 | const { assert!(3 >= 0 && 3 < u32::BITS as _ , "bit number exceeds type bits range"); } |
| 215 | 3 as u8 |
| 216 | }, |
| 217 | set_default: true, |
| 218 | defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: true as u64 }, |
| 219 | ..::common::Zeroable::ZERO |
| 220 | } |
| 221 | ]; |
| 222 | } |
| 223 | } |
| 224 | ); |
| 225 | // Check that `bit` value is used for the bit property with rename when used: |
| 226 | derive_compile!( |
| 227 | derive_device_or_error, |
| 228 | quote! { |
| 229 | #[repr(C)] |
| 230 | #[derive(Device)] |
| 231 | pub struct DummyState { |
| 232 | parent: ParentField<DeviceState>, |
| 233 | #[property(rename = "msi", bit = 3, default = false)] |
| 234 | flags: u64, |
| 235 | } |
| 236 | }, |
| 237 | quote! { |
| 238 | unsafe impl ::hwcore::DevicePropertiesImpl for DummyState { |
| 239 | const PROPERTIES: &'static [::hwcore::bindings::Property] = &[ |
| 240 | ::hwcore::bindings::Property { |
| 241 | name: ::std::ffi::CStr::as_ptr(c"msi"), |
| 242 | info: <u64 as ::hwcore::QDevProp>::BIT_INFO, |
| 243 | offset: ::core::mem::offset_of!(DummyState, flags) as isize, |
| 244 | bitnr : { |
| 245 | const { assert!(3 >= 0 && 3 < u64::BITS as _ , "bit number exceeds type bits range"); } |
| 246 | 3 as u8 |
| 247 | }, |
| 248 | set_default: true, |
| 249 | defval: ::hwcore::bindings::Property__bindgen_ty_1 { u: false as u64 }, |
| 250 | ..::common::Zeroable::ZERO |
| 251 | } |
| 252 | ]; |
| 253 | } |
| 254 | } |
| 255 | ); |
| 256 | } |
| 257 | |
| 258 | #[test] |
| 259 | fn test_derive_object() { |
| 260 | derive_compile_fail!( |
| 261 | derive_object_or_error, |
| 262 | quote! { |
| 263 | #[derive(Object)] |
| 264 | struct Foo { |
| 265 | _unused: [u8; 0], |
| 266 | } |
| 267 | }, |
| 268 | "#[repr(C)] required for #[derive(Object)]" |
| 269 | ); |
| 270 | derive_compile!( |
| 271 | derive_object_or_error, |
| 272 | quote! { |
| 273 | #[derive(Object)] |
| 274 | #[repr(C)] |
| 275 | struct Foo { |
| 276 | _unused: [u8; 0], |
| 277 | } |
| 278 | }, |
| 279 | quote! { |
| 280 | ::common::assert_field_type!( |
| 281 | Foo, |
| 282 | _unused, |
| 283 | ::qom::ParentField<<Foo as ::qom::ObjectImpl>::ParentType> |
| 284 | ); |
| 285 | ::util::module_init! { |
| 286 | MODULE_INIT_QOM => unsafe { |
| 287 | ::qom::type_register_static(&<Foo as ::qom::ObjectImpl>::TYPE_INFO); |
| 288 | } |
| 289 | } |
| 290 | } |
| 291 | ); |
| 292 | } |
| 293 | |
| 294 | #[test] |
| 295 | fn test_derive_tryinto() { |
| 296 | derive_compile_fail!( |
| 297 | derive_tryinto_or_error, |
| 298 | quote! { |
| 299 | #[derive(TryInto)] |
| 300 | struct Foo { |
| 301 | _unused: [u8; 0], |
| 302 | } |
| 303 | }, |
| 304 | "#[repr(u8/u16/u32/u64) required for #[derive(TryInto)]" |
| 305 | ); |
| 306 | derive_compile!( |
| 307 | derive_tryinto_or_error, |
| 308 | quote! { |
| 309 | #[derive(TryInto)] |
| 310 | #[repr(u8)] |
| 311 | enum Foo { |
| 312 | First = 0, |
| 313 | Second, |
| 314 | } |
| 315 | }, |
| 316 | quote! { |
| 317 | impl Foo { |
| 318 | #[allow(dead_code)] |
| 319 | pub const fn into_bits(self) -> u8 { |
| 320 | self as u8 |
| 321 | } |
| 322 | |
| 323 | #[allow(dead_code)] |
| 324 | pub const fn from_bits(value: u8) -> Self { |
| 325 | match ({ |
| 326 | const First: u8 = Foo::First as u8; |
| 327 | const Second: u8 = Foo::Second as u8; |
| 328 | match value { |
| 329 | First => core::result::Result::Ok(Foo::First), |
| 330 | Second => core::result::Result::Ok(Foo::Second), |
| 331 | _ => core::result::Result::Err(value), |
| 332 | } |
| 333 | }) { |
| 334 | Ok(x) => x, |
| 335 | Err(_) => panic!("invalid value for Foo"), |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | impl core::convert::TryFrom<u8> for Foo { |
| 341 | type Error = u8; |
| 342 | |
| 343 | #[allow(ambiguous_associated_items)] |
| 344 | fn try_from(value: u8) -> Result<Self, u8> { |
| 345 | const First: u8 = Foo::First as u8; |
| 346 | const Second: u8 = Foo::Second as u8; |
| 347 | match value { |
| 348 | First => core::result::Result::Ok(Foo::First), |
| 349 | Second => core::result::Result::Ok(Foo::Second), |
| 350 | _ => core::result::Result::Err(value), |
| 351 | } |
| 352 | } |
| 353 | } |
| 354 | } |
| 355 | ); |
| 356 | } |
| 357 | |
| 358 | #[test] |
| 359 | fn test_derive_to_migration_state() { |
| 360 | derive_compile_fail!( |
| 361 | MigrationStateDerive::expand, |
| 362 | quote! { |
| 363 | struct MyStruct { |
| 364 | #[migration_state(omit, clone)] |
| 365 | bad: u32, |
| 366 | } |
| 367 | }, |
| 368 | "ToMigrationState: omit cannot be used with other attributes" |
| 369 | ); |
| 370 | derive_compile_fail!( |
| 371 | MigrationStateDerive::expand, |
| 372 | quote! { |
| 373 | struct MyStruct { |
| 374 | #[migration_state(into)] |
| 375 | bad: u32, |
| 376 | } |
| 377 | }, |
| 378 | "unexpected end of input, expected parentheses" |
| 379 | ); |
| 380 | derive_compile_fail!( |
| 381 | MigrationStateDerive::expand, |
| 382 | quote! { |
| 383 | struct MyStruct { |
| 384 | #[migration_state(into(String), try_into(String))] |
| 385 | bad: &'static str, |
| 386 | } |
| 387 | }, |
| 388 | "ToMigrationState: into and try_into attributes cannot be used together" |
| 389 | ); |
| 390 | derive_compile!( |
| 391 | MigrationStateDerive::expand, |
| 392 | quote! { |
| 393 | #[migration_state(rename = CustomMigration)] |
| 394 | struct MyStruct { |
| 395 | #[migration_state(omit)] |
| 396 | runtime_field: u32, |
| 397 | |
| 398 | #[migration_state(clone)] |
| 399 | shared_data: String, |
| 400 | |
| 401 | #[migration_state(into(Cow<'static, str>), clone)] |
| 402 | converted_field: String, |
| 403 | |
| 404 | #[migration_state(try_into(i8))] |
| 405 | fallible_field: u32, |
| 406 | |
| 407 | nested_field: NestedStruct, |
| 408 | simple_field: u32, |
| 409 | } |
| 410 | }, |
| 411 | quote! { |
| 412 | #[derive(Default)] |
| 413 | pub struct CustomMigration { |
| 414 | pub shared_data: String, |
| 415 | pub converted_field: Cow<'static, str>, |
| 416 | pub fallible_field: i8, |
| 417 | pub nested_field: <NestedStruct as ToMigrationState>::Migrated, |
| 418 | pub simple_field: <u32 as ToMigrationState>::Migrated, |
| 419 | } |
| 420 | impl ToMigrationState for MyStruct { |
| 421 | type Migrated = CustomMigration; |
| 422 | fn snapshot_migration_state( |
| 423 | &self, |
| 424 | target: &mut Self::Migrated |
| 425 | ) -> Result<(), migration::InvalidError> { |
| 426 | target.shared_data = self.shared_data.clone(); |
| 427 | target.converted_field = self.converted_field.clone().into(); |
| 428 | target.fallible_field = self |
| 429 | .fallible_field |
| 430 | .try_into() |
| 431 | .map_err(|_| migration::InvalidError)?; |
| 432 | self.nested_field |
| 433 | .snapshot_migration_state(&mut target.nested_field)?; |
| 434 | self.simple_field |
| 435 | .snapshot_migration_state(&mut target.simple_field)?; |
| 436 | Ok(()) |
| 437 | } |
| 438 | #[allow(clippy::used_underscore_binding)] |
| 439 | fn restore_migrated_state_mut( |
| 440 | &mut self, |
| 441 | source: Self::Migrated, |
| 442 | _version_id: u8 |
| 443 | ) -> Result<(), migration::InvalidError> { |
| 444 | let Self::Migrated { |
| 445 | shared_data, |
| 446 | converted_field, |
| 447 | fallible_field, |
| 448 | nested_field, |
| 449 | simple_field |
| 450 | } = source; |
| 451 | self.shared_data = shared_data; |
| 452 | self.converted_field = converted_field.into(); |
| 453 | self.fallible_field = fallible_field |
| 454 | .try_into() |
| 455 | .map_err(|_| migration::InvalidError)?; |
| 456 | self.nested_field |
| 457 | .restore_migrated_state_mut(nested_field, _version_id)?; |
| 458 | self.simple_field |
| 459 | .restore_migrated_state_mut(simple_field, _version_id)?; |
| 460 | Ok(()) |
| 461 | } |
| 462 | } |
| 463 | } |
| 464 | ); |
| 465 | } |