use anyhow::{bail, Context};
use chrono::NaiveDate;
use itertools::Itertools;
use serde::de::{Error, Unexpected};
use serde::{Deserialize, Deserializer, Serialize};
use std::array::TryFromSliceError;
use std::collections::BTreeMap;
use std::fmt;
use tracing::error;
const VERSION_HISTORY_SCHEMA_ID: &str = "https://fuchsia.dev/schema/version_history.json";
const VERSION_HISTORY_NAME: &str = "Platform version map";
const VERSION_HISTORY_TYPE: &str = "version_history";
#[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct ApiLevel(u32);
impl ApiLevel {
pub const NEXT: ApiLevel = ApiLevel(4291821568);
pub const HEAD: ApiLevel = ApiLevel(4292870144);
pub const PLATFORM: ApiLevel = ApiLevel(4293918720);
pub const fn from_u32(value: u32) -> Self {
Self(value)
}
pub fn as_u32(&self) -> u32 {
self.0
}
#[deprecated = "API levels are 32-bits as of RFC-0246"]
pub fn as_u64(&self) -> u64 {
self.0 as u64
}
}
impl fmt::Debug for ApiLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ApiLevel::NEXT => write!(f, "ApiLevel::NEXT"),
ApiLevel::HEAD => write!(f, "ApiLevel::HEAD"),
ApiLevel::PLATFORM => write!(f, "ApiLevel::PLATFORM"),
ApiLevel(l) => f.debug_tuple("ApiLevel").field(&l).finish(),
}
}
}
impl fmt::Display for ApiLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ApiLevel::NEXT => write!(f, "NEXT"),
ApiLevel::HEAD => write!(f, "HEAD"),
ApiLevel::PLATFORM => write!(f, "PLATFORM"),
ApiLevel(l) => write!(f, "{}", l),
}
}
}
impl std::str::FromStr for ApiLevel {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"NEXT" => Ok(ApiLevel::NEXT),
"HEAD" => Ok(ApiLevel::HEAD),
"PLATFORM" => Ok(ApiLevel::PLATFORM),
s => Ok(ApiLevel::from_u32(s.parse()?)),
}
}
}
impl From<u32> for ApiLevel {
fn from(api_level: u32) -> ApiLevel {
ApiLevel(api_level)
}
}
impl From<&u32> for ApiLevel {
fn from(api_level: &u32) -> ApiLevel {
ApiLevel(*api_level)
}
}
impl From<ApiLevel> for u32 {
fn from(api_level: ApiLevel) -> u32 {
api_level.0
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct AbiRevision(u64);
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum AbiRevisionExplanation {
Normal,
Platform {
date: chrono::NaiveDate,
git_revision: u16,
},
Unstable {
date: chrono::NaiveDate,
git_revision: u16,
},
Invalid,
Malformed,
}
impl AbiRevision {
pub const INVALID: AbiRevision = AbiRevision(0xFFFF_FFFF_FFFF_FFFF);
pub const PATH: &'static str = "meta/fuchsia.abi/abi-revision";
pub fn from_bytes(b: [u8; 8]) -> Self {
AbiRevision(u64::from_le_bytes(b))
}
pub fn as_bytes(&self) -> [u8; 8] {
self.0.to_le_bytes()
}
pub const fn from_u64(u: u64) -> AbiRevision {
AbiRevision(u)
}
pub fn as_u64(&self) -> u64 {
self.0
}
pub fn explanation(self) -> AbiRevisionExplanation {
const SPECIAL_ABI_REVISION_PREFIX: u64 = 0xFF;
const UNSTABLE_ABI_REVISION_PREFIX: u64 = 0xFF00;
const PLATFORM_ABI_REVISION_PREFIX: u64 = 0xFF01;
let get_date = || {
const DATE_MASK: u64 = 0x0000_0000_FFFF_FFFF;
let date_ordinal = (self.as_u64() & DATE_MASK) as i32;
chrono::NaiveDate::from_num_days_from_ce_opt(date_ordinal).unwrap_or_default()
};
let get_git_revision = || {
const GIT_HASH_MASK: u64 = 0x0000_FFFF_0000_0000;
((self.as_u64() & GIT_HASH_MASK) >> 32) as u16
};
if self.as_u64() >> 56 != SPECIAL_ABI_REVISION_PREFIX {
AbiRevisionExplanation::Normal
} else if self.as_u64() >> 48 == UNSTABLE_ABI_REVISION_PREFIX {
AbiRevisionExplanation::Unstable { date: get_date(), git_revision: get_git_revision() }
} else if self.as_u64() >> 48 == PLATFORM_ABI_REVISION_PREFIX {
AbiRevisionExplanation::Platform { date: get_date(), git_revision: get_git_revision() }
} else if self == AbiRevision::INVALID {
AbiRevisionExplanation::Invalid
} else {
AbiRevisionExplanation::Malformed
}
}
}
impl fmt::Display for AbiRevision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:x}", self.0)
}
}
impl std::str::FromStr for AbiRevision {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = if let Some(s) = s.strip_prefix("0x") {
u64::from_str_radix(&s, 16)?
} else {
u64::from_str_radix(&s, 10)?
};
Ok(AbiRevision::from_u64(parsed))
}
}
impl<'de> Deserialize<'de> for AbiRevision {
fn deserialize<D>(deserializer: D) -> Result<AbiRevision, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &"an unsigned integer"))
}
}
impl From<u64> for AbiRevision {
fn from(abi_revision: u64) -> AbiRevision {
AbiRevision(abi_revision)
}
}
impl From<&u64> for AbiRevision {
fn from(abi_revision: &u64) -> AbiRevision {
AbiRevision(*abi_revision)
}
}
impl From<AbiRevision> for u64 {
fn from(abi_revision: AbiRevision) -> u64 {
abi_revision.0
}
}
impl From<[u8; 8]> for AbiRevision {
fn from(abi_revision: [u8; 8]) -> AbiRevision {
AbiRevision::from_bytes(abi_revision)
}
}
impl TryFrom<&[u8]> for AbiRevision {
type Error = TryFromSliceError;
fn try_from(abi_revision: &[u8]) -> Result<AbiRevision, Self::Error> {
let abi_revision: [u8; 8] = abi_revision.try_into()?;
Ok(AbiRevision::from_bytes(abi_revision))
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct Version {
pub api_level: ApiLevel,
pub abi_revision: AbiRevision,
pub status: Status,
}
impl Version {
fn is_runnable(&self) -> bool {
match self.status {
Status::InDevelopment | Status::Supported => true,
Status::Unsupported => false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash)]
pub enum Status {
#[serde(rename = "in-development")]
InDevelopment,
#[serde(rename = "supported")]
Supported,
#[serde(rename = "unsupported")]
Unsupported,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VersionHistory {
versions: &'static [Version],
}
impl VersionHistory {
pub const fn new(versions: &'static [Version]) -> Self {
VersionHistory { versions }
}
pub fn get_abi_revision_for_platform_components(&self) -> AbiRevision {
self.version_from_api_level(ApiLevel::PLATFORM)
.expect("API Level PLATFORM not found!")
.abi_revision
}
pub fn get_misleading_version_for_ffx(&self) -> Version {
self.runnable_versions()
.filter(|v| {
v.api_level != ApiLevel::NEXT
&& v.api_level != ApiLevel::HEAD
&& v.api_level != ApiLevel::PLATFORM
})
.last()
.unwrap()
}
pub fn get_example_supported_version_for_tests(&self) -> Version {
self.runnable_versions()
.filter(|v| {
v.api_level != ApiLevel::NEXT
&& v.api_level != ApiLevel::HEAD
&& v.api_level != ApiLevel::PLATFORM
})
.last()
.unwrap()
}
pub fn check_api_level_for_build(
&self,
api_level: ApiLevel,
) -> Result<AbiRevision, ApiLevelError> {
let Some(version) = self.version_from_api_level(api_level) else {
return Err(ApiLevelError::Unknown {
api_level,
supported: self.runnable_versions().map(|v| v.api_level).collect(),
});
};
if version.is_runnable() {
Ok(version.abi_revision)
} else {
Err(ApiLevelError::Unsupported {
version,
supported: self.runnable_versions().map(|v| v.api_level).collect(),
})
}
}
pub fn check_abi_revision_for_runtime(
&self,
abi_revision: AbiRevision,
) -> Result<(), AbiRevisionError> {
if let Some(version) = self.version_from_abi_revision(abi_revision) {
if version.is_runnable() {
Ok(())
} else {
Err(AbiRevisionError::Retired {
version,
supported_versions: self.supported_versions(),
})
}
} else {
match abi_revision.explanation() {
AbiRevisionExplanation::Platform { date, git_revision } => {
let (platform_date, platform_commit_hash) = self.platform_abi_info();
Err(AbiRevisionError::PlatformMismatch {
abi_revision,
package_date: date,
package_commit_hash: git_revision,
platform_date,
platform_commit_hash,
})
}
AbiRevisionExplanation::Unstable { date, git_revision } => {
let (platform_date, platform_commit_hash) = self.platform_abi_info();
Err(AbiRevisionError::UnstableMismatch {
abi_revision,
package_sdk_date: date,
package_sdk_commit_hash: git_revision,
platform_date,
platform_commit_hash,
supported_versions: self.supported_versions(),
})
}
AbiRevisionExplanation::Malformed => {
Err(AbiRevisionError::Malformed { abi_revision })
}
AbiRevisionExplanation::Invalid => Err(AbiRevisionError::Invalid),
AbiRevisionExplanation::Normal => Err(AbiRevisionError::TooNew {
abi_revision,
supported_versions: self.supported_versions(),
}),
}
}
}
fn runnable_versions(&self) -> impl Iterator<Item = Version> + '_ {
self.versions.iter().filter(|v| v.is_runnable()).cloned()
}
fn supported_versions(&self) -> VersionVec {
VersionVec(
self.versions
.iter()
.filter(|v| match v.status {
Status::InDevelopment => false,
Status::Supported => true,
Status::Unsupported => false,
})
.cloned()
.collect(),
)
}
fn version_from_abi_revision(&self, abi_revision: AbiRevision) -> Option<Version> {
self.versions.iter().find(|v| v.abi_revision == abi_revision).cloned()
}
pub fn version_from_api_level(&self, api_level: ApiLevel) -> Option<Version> {
self.versions.iter().find(|v| v.api_level == api_level).cloned()
}
fn platform_abi_info(&self) -> (NaiveDate, u16) {
if let Some(platform_version) = self.version_from_api_level(ApiLevel::PLATFORM) {
if let AbiRevisionExplanation::Platform { date, git_revision } =
platform_version.abi_revision.explanation()
{
return (date, git_revision);
}
}
error!(
"No PLATFORM API level found. This should never happen.
Returning bogus data instead of panicking."
);
(NaiveDate::default(), 0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AbiRevisionError {
#[error(
"Unknown platform ABI revision: 0x{abi_revision}.
Found platform component from {package_date} (Git hash {package_commit_hash:x}).
Platform components must be from {platform_date} (Git hash {platform_commit_hash:x})"
)]
PlatformMismatch {
abi_revision: AbiRevision,
package_date: NaiveDate,
package_commit_hash: u16,
platform_date: NaiveDate,
platform_commit_hash: u16,
},
#[error(
"Unknown NEXT or HEAD ABI revision: 0x{abi_revision}.
SDK is from {package_sdk_date} (Git hash {package_sdk_commit_hash:x}).
Expected {platform_date} (Git hash {platform_commit_hash:x})
The following API levels are stable and supported:{supported_versions}"
)]
UnstableMismatch {
abi_revision: AbiRevision,
package_sdk_date: NaiveDate,
package_sdk_commit_hash: u16,
platform_date: NaiveDate,
platform_commit_hash: u16,
supported_versions: VersionVec,
},
#[error(
"Unknown ABI revision 0x{abi_revision}. It was probably created after
this platform or tool was built. The following API levels are stable and
supported:{supported_versions}"
)]
TooNew { abi_revision: AbiRevision, supported_versions: VersionVec },
#[error("Retired API level {} (0x{}) cannot run.
The following API levels are stable and supported:{supported_versions}",
.version.api_level, .version.abi_revision)]
Retired { version: Version, supported_versions: VersionVec },
#[error("Invalid ABI revision 0x{}.", AbiRevision::INVALID)]
Invalid,
#[error("ABI revision 0x{abi_revision} has an unrecognized format.")]
Malformed { abi_revision: AbiRevision },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionVec(pub Vec<Version>);
impl std::fmt::Display for VersionVec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for version in self.0.iter() {
write!(f, "\n└── {} (0x{})", version.api_level, version.abi_revision)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiLevelError {
Unknown { api_level: ApiLevel, supported: Vec<ApiLevel> },
Unsupported { version: Version, supported: Vec<ApiLevel> },
}
impl std::error::Error for ApiLevelError {}
impl std::fmt::Display for ApiLevelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiLevelError::Unknown { api_level, supported } => {
write!(
f,
"Unknown target API level: {}. Is the SDK too old to support it?
The following API levels are supported: {}",
api_level,
supported.iter().join(", ")
)
}
ApiLevelError::Unsupported { version, supported } => {
write!(
f,
"The SDK no longer supports API level {}.
The following API levels are supported: {}",
version.api_level,
supported.iter().join(", ")
)
}
}
}
}
#[derive(Deserialize)]
struct VersionHistoryDataJson {
name: String,
#[serde(rename = "type")]
element_type: String,
api_levels: BTreeMap<String, ApiLevelJson>,
special_api_levels: BTreeMap<String, SpecialApiLevelJson>,
}
#[derive(Deserialize)]
struct VersionHistoryJson {
schema_id: String,
data: VersionHistoryDataJson,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deserialize)]
struct ApiLevelJson {
pub abi_revision: AbiRevision,
pub status: Status,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deserialize)]
struct SpecialApiLevelJson {
pub as_u32: u32,
pub abi_revision: AbiRevision,
pub status: Status,
}
pub fn parse_version_history(bytes: &[u8]) -> anyhow::Result<Vec<Version>> {
let v: VersionHistoryJson = serde_json::from_slice(bytes)?;
if v.schema_id != VERSION_HISTORY_SCHEMA_ID {
bail!("expected schema_id = {:?}; got {:?}", VERSION_HISTORY_SCHEMA_ID, v.schema_id)
}
if v.data.name != VERSION_HISTORY_NAME {
bail!("expected data.name = {:?}; got {:?}", VERSION_HISTORY_NAME, v.data.name)
}
if v.data.element_type != VERSION_HISTORY_TYPE {
bail!("expected data.type = {:?}; got {:?}", VERSION_HISTORY_TYPE, v.data.element_type,)
}
let mut versions = Vec::new();
for (key, value) in v.data.api_levels {
versions.push(Version {
api_level: key.parse()?,
abi_revision: value.abi_revision,
status: value.status,
});
}
for (key, value) in v.data.special_api_levels {
let api_level: ApiLevel =
key.parse().with_context(|| format!("Unknown special API level: {}", key))?;
if api_level.as_u32() != value.as_u32 {
bail!(
"Special API level {} had unexpected numerical value {} (Expected {})",
api_level,
value.as_u32,
api_level.as_u32()
)
}
versions.push(Version {
api_level,
abi_revision: value.abi_revision,
status: value.status,
});
}
versions.sort_by_key(|s| s.api_level);
let Some(latest_api_version) = versions.last() else {
bail!("there must be at least one API level")
};
if latest_api_version.status == Status::Unsupported {
bail!("most recent API level must not be 'unsupported'")
}
Ok(versions)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_history_works() {
let expected_bytes = br#"{
"data": {
"name": "Platform version map",
"type": "version_history",
"api_levels": {
"1":{
"abi_revision":"0xBEBE3F5CAAA046D2",
"status":"supported"
},
"2":{
"abi_revision":"0x50cbc6e8a39e1e2c",
"status":"in-development"
}
},
"special_api_levels": {
"NEXT": {
"as_u32": 4291821568,
"abi_revision": "0xFF0038FA031D0347",
"status": "in-development"
},
"HEAD": {
"as_u32": 4292870144,
"abi_revision": "0xFF0038FA031D0348",
"status": "in-development"
},
"PLATFORM": {
"as_u32": 4293918720,
"abi_revision": "0xFF01A9328DA0C138",
"status": "in-development"
}
}
},
"schema_id": "https://fuchsia.dev/schema/version_history.json"
}"#;
assert_eq!(
parse_version_history(&expected_bytes[..]).unwrap(),
vec![
Version {
api_level: 1.into(),
abi_revision: 0xBEBE3F5CAAA046D2.into(),
status: Status::Supported
},
Version {
api_level: 2.into(),
abi_revision: 0x50CBC6E8A39E1E2C.into(),
status: Status::InDevelopment
},
Version {
api_level: ApiLevel::NEXT,
abi_revision: 0xFF0038FA031D0347.into(),
status: Status::InDevelopment
},
Version {
api_level: ApiLevel::HEAD,
abi_revision: 0xFF0038FA031D0348.into(),
status: Status::InDevelopment
},
Version {
api_level: ApiLevel::PLATFORM,
abi_revision: 0xFF01A9328DA0C138.into(),
status: Status::InDevelopment
},
],
);
}
#[test]
fn test_parse_history_rejects_invalid_schema() {
let expected_bytes = br#"{
"data": {
"name": "Platform version map",
"type": "version_history",
"api_levels": {},
"special_api_levels": {}
},
"schema_id": "some-schema"
}"#;
assert_eq!(
&parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
r#"expected schema_id = "https://fuchsia.dev/schema/version_history.json"; got "some-schema""#
);
}
#[test]
fn test_parse_history_rejects_invalid_name() {
let expected_bytes = br#"{
"data": {
"name": "some-name",
"type": "version_history",
"api_levels": {},
"special_api_levels": {}
},
"schema_id": "https://fuchsia.dev/schema/version_history.json"
}"#;
assert_eq!(
&parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
r#"expected data.name = "Platform version map"; got "some-name""#
);
}
#[test]
fn test_parse_history_rejects_invalid_type() {
let expected_bytes = br#"{
"data": {
"name": "Platform version map",
"type": "some-type",
"api_levels": {},
"special_api_levels": {}
},
"schema_id": "https://fuchsia.dev/schema/version_history.json"
}"#;
assert_eq!(
&parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
r#"expected data.type = "version_history"; got "some-type""#
);
}
#[test]
fn test_parse_history_rejects_invalid_versions() {
for (api_level, abi_revision, err) in [
(
"some-version",
"1",
"invalid digit found in string" ,
),
(
"-1",
"1",
"invalid digit found in string" ,
),
(
"1",
"some-revision",
"invalid value: string \"some-revision\", expected an unsigned integer at line 1 column 58",
),
(
"1",
"-1",
"invalid value: string \"-1\", expected an unsigned integer at line 1 column 47",
),
] {
let expected_bytes = serde_json::to_vec(&serde_json::json!({
"data": {
"name": VERSION_HISTORY_NAME,
"type": VERSION_HISTORY_TYPE,
"api_levels": {
api_level:{
"abi_revision": abi_revision,
"status": Status::InDevelopment,
}
},
"special_api_levels": {},
},
"schema_id": VERSION_HISTORY_SCHEMA_ID,
}))
.unwrap();
assert_eq!(parse_version_history(&expected_bytes[..]).unwrap_err().to_string(), err);
}
}
#[test]
fn test_parse_history_rejects_bogus_special_levels() {
let input = br#"{
"data": {
"name": "Platform version map",
"type": "version_history",
"api_levels": {},
"special_api_levels": {
"WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN": {
"as_u32": 1234,
"abi_revision": "0x50cbc6e8a39e1e2c",
"status": "in-development"
}
}
},
"schema_id": "https://fuchsia.dev/schema/version_history.json"
}"#;
assert_eq!(
parse_version_history(&input[..]).unwrap_err().to_string(),
"Unknown special API level: WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN"
);
}
#[test]
fn test_parse_history_rejects_misnumbered_special_levels() {
let input = br#"{
"data": {
"name": "Platform version map",
"type": "version_history",
"api_levels": {},
"special_api_levels": {
"HEAD": {
"as_u32": 1234,
"abi_revision": "0x50cbc6e8a39e1e2c",
"status": "in-development"
}
}
},
"schema_id": "https://fuchsia.dev/schema/version_history.json"
}"#;
assert_eq!(
parse_version_history(&input[..]).unwrap_err().to_string(),
"Special API level HEAD had unexpected numerical value 1234 (Expected 4292870144)"
);
}
pub const FAKE_VERSION_HISTORY: VersionHistory = VersionHistory {
versions: &[
Version {
api_level: ApiLevel::from_u32(4),
abi_revision: AbiRevision::from_u64(0x58ea445e942a0004),
status: Status::Unsupported,
},
Version {
api_level: ApiLevel::from_u32(5),
abi_revision: AbiRevision::from_u64(0x58ea445e942a0005),
status: Status::Supported,
},
Version {
api_level: ApiLevel::from_u32(6),
abi_revision: AbiRevision::from_u64(0x58ea445e942a0006),
status: Status::Supported,
},
Version {
api_level: ApiLevel::from_u32(7),
abi_revision: AbiRevision::from_u64(0x58ea445e942a0007),
status: Status::Supported,
},
Version {
api_level: ApiLevel::NEXT,
abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
status: Status::InDevelopment,
},
Version {
api_level: ApiLevel::HEAD,
abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
status: Status::InDevelopment,
},
Version {
api_level: ApiLevel::PLATFORM,
abi_revision: AbiRevision::from_u64(0xFF01_8C4D_000B_4751),
status: Status::InDevelopment,
},
],
};
#[test]
fn test_check_abi_revision() {
let supported_versions =
VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
assert_eq!(
FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF01_ABCD_000B_2224.into()),
Err(AbiRevisionError::PlatformMismatch {
abi_revision: 0xFF01_ABCD_000B_2224.into(),
package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
package_commit_hash: 0xABCD,
platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
platform_commit_hash: 0x8C4D,
})
);
assert_eq!(
FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF00_ABCD_000B_2224.into()),
Err(AbiRevisionError::UnstableMismatch {
abi_revision: 0xFF00_ABCD_000B_2224.into(),
package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
package_sdk_commit_hash: 0xABCD,
platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
platform_commit_hash: 0x8C4D,
supported_versions: supported_versions.clone()
})
);
assert_eq!(
FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x1234.into()),
Err(AbiRevisionError::TooNew {
abi_revision: 0x1234.into(),
supported_versions: supported_versions.clone()
})
);
assert_eq!(
FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x58ea445e942a0004.into()),
Err(AbiRevisionError::Retired {
version: FAKE_VERSION_HISTORY.versions[0].clone(),
supported_versions: supported_versions.clone()
})
);
assert_eq!(
FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(AbiRevision::INVALID),
Err(AbiRevisionError::Invalid)
);
assert_eq!(
FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF02_0000_0000_0000.into()),
Err(AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() })
);
FAKE_VERSION_HISTORY
.check_abi_revision_for_runtime(0x58ea445e942a0005.into())
.expect("level 5 should be supported");
FAKE_VERSION_HISTORY
.check_abi_revision_for_runtime(0x58ea445e942a0007.into())
.expect("level 7 should be supported");
}
#[test]
fn test_pretty_print_abi_error() {
let supported_versions =
VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
assert_eq!(
AbiRevisionError::PlatformMismatch {
abi_revision: 0xFF01_ABCD_000B_2224.into(),
package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
package_commit_hash: 0xABCD,
platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
platform_commit_hash: 0x8C4D,
}
.to_string(),
"Unknown platform ABI revision: 0xff01abcd000b2224.
Found platform component from 1998-09-04 (Git hash abcd).
Platform components must be from 2024-09-24 (Git hash 8c4d)"
);
assert_eq!(
AbiRevisionError::UnstableMismatch {
abi_revision: 0xFF00_ABCD_000B_2224.into(),
package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
package_sdk_commit_hash: 0xABCD,
platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
platform_commit_hash: 0x8C4D,
supported_versions: supported_versions.clone()
}
.to_string(),
"Unknown NEXT or HEAD ABI revision: 0xff00abcd000b2224.
SDK is from 1998-09-04 (Git hash abcd).
Expected 2024-09-24 (Git hash 8c4d)
The following API levels are stable and supported:
└── 5 (0x58ea445e942a0005)
└── 6 (0x58ea445e942a0006)
└── 7 (0x58ea445e942a0007)"
);
assert_eq!(
AbiRevisionError::TooNew {
abi_revision: 0x1234.into(),
supported_versions: supported_versions.clone()
}
.to_string(),
"Unknown ABI revision 0x1234. It was probably created after
this platform or tool was built. The following API levels are stable and
supported:
└── 5 (0x58ea445e942a0005)
└── 6 (0x58ea445e942a0006)
└── 7 (0x58ea445e942a0007)"
);
assert_eq!(
AbiRevisionError::Retired {
version: FAKE_VERSION_HISTORY.versions[0].clone(),
supported_versions: supported_versions.clone()
}
.to_string(),
"Retired API level 4 (0x58ea445e942a0004) cannot run.
The following API levels are stable and supported:
└── 5 (0x58ea445e942a0005)
└── 6 (0x58ea445e942a0006)
└── 7 (0x58ea445e942a0007)"
);
assert_eq!(
AbiRevisionError::Invalid.to_string(),
"Invalid ABI revision 0xffffffffffffffff."
);
assert_eq!(
AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() }.to_string(),
"ABI revision 0xff02000000000000 has an unrecognized format."
);
}
#[test]
fn test_pretty_print_api_error() {
let supported: Vec<ApiLevel> =
vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
assert_eq!(
ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() }
.to_string(),
"Unknown target API level: 42. Is the SDK too old to support it?
The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM",
);
assert_eq!(
ApiLevelError::Unsupported {
version: FAKE_VERSION_HISTORY.versions[0].clone(),
supported: supported.clone()
}
.to_string(),
"The SDK no longer supports API level 4.
The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM"
);
}
#[test]
fn test_check_api_level() {
let supported: Vec<ApiLevel> =
vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(42.into()),
Err(ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() })
);
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(4.into()),
Err(ApiLevelError::Unsupported {
version: FAKE_VERSION_HISTORY.versions[0].clone(),
supported: supported.clone()
})
);
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(6.into()),
Ok(0x58ea445e942a0006.into())
);
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(7.into()),
Ok(0x58ea445e942a0007.into())
);
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::NEXT),
Ok(0xFF00_8C4D_000B_4751.into())
);
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::HEAD),
Ok(0xFF00_8C4D_000B_4751.into())
);
assert_eq!(
FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::PLATFORM),
Ok(0xFF01_8C4D_000B_4751.into())
);
}
#[test]
fn test_various_getters() {
assert_eq!(
FAKE_VERSION_HISTORY.get_abi_revision_for_platform_components(),
0xFF01_8C4D_000B_4751.into()
);
assert_eq!(
FAKE_VERSION_HISTORY.get_misleading_version_for_ffx(),
FAKE_VERSION_HISTORY.versions[3].clone()
);
assert_eq!(FAKE_VERSION_HISTORY.get_misleading_version_for_ffx().api_level, 7.into());
assert_eq!(
FAKE_VERSION_HISTORY.get_example_supported_version_for_tests(),
FAKE_VERSION_HISTORY.versions[FAKE_VERSION_HISTORY.versions.len() - 4].clone()
);
assert_eq!(
FAKE_VERSION_HISTORY.get_example_supported_version_for_tests().api_level,
7.into()
);
}
#[test]
fn test_explanations() {
let exp = |abi_revision| AbiRevision::from_u64(abi_revision).explanation();
assert_eq!(exp(0x1234_5678_9abc_deff), AbiRevisionExplanation::Normal);
assert_eq!(
exp(0xFF01_abcd_00ab_eeee),
AbiRevisionExplanation::Platform {
date: chrono::NaiveDate::from_num_days_from_ce_opt(0xab_eeee).unwrap(),
git_revision: 0xabcd
}
);
assert_eq!(
exp(0xFF00_1234_0012_3456),
AbiRevisionExplanation::Unstable {
date: chrono::NaiveDate::from_num_days_from_ce_opt(0x12_3456).unwrap(),
git_revision: 0x1234
}
);
assert_eq!(
exp(0xFF00_1234_abcd_9876),
AbiRevisionExplanation::Unstable {
date: chrono::NaiveDate::default(),
git_revision: 0x1234
}
);
assert_eq!(
exp(0xFF01_1234_abcd_9876),
AbiRevisionExplanation::Platform {
date: chrono::NaiveDate::default(),
git_revision: 0x1234
}
);
assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFF), AbiRevisionExplanation::Invalid);
assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFE), AbiRevisionExplanation::Malformed);
}
}