Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion console_backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ serde-pickle = { version = "0.6.2", optional = true }
parking_lot = "0.11.1"
minreq = { version = "2.4.2", features = ["https"] }
regex = { version = "1.5.4" }
semver = { version = "1" }
rust-ini = "0.17.0"
sbp = { version = "4.0.2", features = ["json", "link", "swiftnav"] }
sbp-settings = "0.3"
Expand Down
1 change: 1 addition & 0 deletions console_backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod shared_state;
pub mod solution_tab;
pub mod solution_velocity_tab;
pub mod status_bar;
pub mod swift_version;
pub mod tracking_signals_tab;
pub mod tracking_sky_plot_tab;
pub mod types;
Expand Down
282 changes: 282 additions & 0 deletions console_backend/src/swift_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
use std::{cmp::Ordering, str::FromStr};

use anyhow::anyhow;
use lazy_static::lazy_static;
use regex::Regex;

// Release firmwares are prefixed with a 'v' (eg v2.5.0)
const RELEASE_NAMESPACE: &str = "v";

/// Represents a version as generated by git. This string will be in the format
/// <namespace><marketing>.<major>.<minor>(-dev) where marketing, major, and
/// minor are integers and dev is an optional string that is only present for
/// non-release builds.
///
/// The marketing, major, and minor components are separated out and stored in
/// appropriately named properties. The remaining parts of the string are
/// combined into the dev string property. In practice this string will consist
/// of a leading 'v' characters, plus whatever characters trailed the minor
/// number. The devstring property will therefore always contain at least 1
/// character, a minimum of the leading 'v'. If the devstring contains /only/ a
/// 'v' the version string is not considered to be a development build, but if
/// the dev string is any longer the build is a development build and the
/// 'isdev' property will return True.
#[derive(Debug, PartialEq, Eq)]
pub struct SwiftVersion {
marketing: u64,
major: u64,
minor: u64,
namespace: String,
dev: String,
}

impl FromStr for SwiftVersion {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
lazy_static! {
// This regex separates out components of the input string into
// regex match groups. The components are:
//
// * namespace: Any number of non-digit characters
// * marketing - Integer (1 of more characters, 0-9)
// * major - Integer
// * minor - Integer
// * dev - Any number of characters
//
// Leading whitespace is stripped away, any trailing whitespace is
// included in the 'dev' group.
static ref VERSION_RE: Regex = Regex::new(r"^\s*(?P<namespace>[^0-9]*)(?P<marketing>[0-9]+)\.(?P<major>[0-9]+)\.(?P<minor>[0-9]+)(?P<dev>.*)$").unwrap();
}

let captured = VERSION_RE
.captures(s)
.ok_or_else(|| anyhow!("Invalid version: {}", s))?;
let marketing = captured
.name("marketing")
.ok_or_else(|| anyhow!("Could not find marketing version for {}", s))?
.as_str()
.parse::<u64>()
.map_err(|e| anyhow!("Could not parse marketing version: {}", e.to_string()))?;
let major = captured
.name("major")
.ok_or_else(|| anyhow!("Could not find major version for {}", s))?
.as_str()
.parse::<u64>()
.map_err(|e| anyhow!("Could not parse major version: {}", e.to_string()))?;
let minor = captured
.name("minor")
.ok_or_else(|| anyhow!("Could not find minor version for {}", s))?
.as_str()
.parse::<u64>()
.map_err(|e| anyhow!("Could not parse minor version: {}", e.to_string()))?;

let namespace = captured.name("namespace");
let dev = captured.name("dev");
let mut dev_string = String::new();
let mut namespace_string = String::new();

if let Some(namespace) = namespace {
namespace_string += namespace.as_str();
}

if let Some(dev) = dev {
dev_string += dev.as_str();
}

Ok(SwiftVersion {
marketing,
major,
minor,
namespace: namespace_string,
dev: dev_string,
})
}
}

impl SwiftVersion {
pub const fn new(
marketing: u64,
major: u64,
minor: u64,
namespace: String,
dev: String,
) -> Self {
SwiftVersion {
marketing,
major,
minor,
namespace,
dev,
}
}

pub fn parse(text: &str) -> Result<Self, anyhow::Error> {
SwiftVersion::from_str(text)
}

pub fn parse_filename(text: &str) -> Result<Self, anyhow::Error> {
lazy_static! {
static ref FILENAME_RE: Regex =
Regex::new(r"PiksiMulti-(.*v[0-9]*\.[0-9]*\.[0-9]*.*).bin$").unwrap();
}

let captured = FILENAME_RE
.captures(text)
.ok_or_else(|| anyhow!("Invalid filename {}", text))?;

let version = captured
.get(1)
.ok_or_else(|| anyhow!("Could not get version from filename {}", text))?
.as_str();

SwiftVersion::parse(version)
}

pub fn is_dev(&self) -> bool {
// A namespace string of consisting of a single 'v' character is not
// considered to be a development version as release versions contain
// this namespace. All other namespaces (eg: INTERNAL-v or starling-v)
// are considered to be development versions, as are any versions with
// trailing development tags
!self.dev.is_empty() || (!self.namespace.is_empty() && self.namespace != RELEASE_NAMESPACE)
}
}

impl PartialOrd for SwiftVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.dev != other.dev || self.namespace != other.namespace {
return None;
}

let marketing_ord = self.marketing.cmp(&other.marketing);
if marketing_ord != Ordering::Equal {
return Some(marketing_ord);
}

let major_ord = self.major.cmp(&other.major);
if major_ord != Ordering::Equal {
return Some(major_ord);
}

let minor_ord = self.minor.cmp(&other.minor);
if minor_ord != Ordering::Equal {
return Some(minor_ord);
}

Some(Ordering::Equal)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_success_cases() {
#[rustfmt::skip]
let success_test_cases = [
("v2.1.0", 2, 1, 0, "v", "", false),
("v2.2.17-develop", 2, 2, 17, "v", "-develop", true),
("v99.99.99-arbitrary-string", 99, 99, 99, "v", "-arbitrary-string", true),
("v1.1.1 including some spaces", 1, 1, 1, "v", " including some spaces", true),
(" v2.0.0", 2, 0, 0, "v", "", false),
("1.2.3.4", 1, 2, 3, "", ".4", true)
];

for (version_str, marketing, major, minor, namespace, dev, isdev) in success_test_cases {
let ver = SwiftVersion::parse(version_str).unwrap();
assert_eq!(ver.marketing, marketing);
assert_eq!(ver.major, major);
assert_eq!(ver.minor, minor);
assert_eq!(ver.dev, dev);
assert_eq!(ver.namespace, namespace);
assert_eq!(ver.is_dev(), isdev);
}
}

#[test]
fn test_filename_cases() {
#[rustfmt::skip]
let success_test_cases = [
("PiksiMulti-v2.1.0.bin", 2, 1, 0, "v", "", false),
("PiksiMulti-INTERNAL-starling-v1.5.0-develop-2021082401-3.bin", 1, 5, 0, "INTERNAL-starling-v", "-develop-2021082401-3", true),
("PiksiMulti-INTERNAL-v2.5.4.bin", 2, 5, 4, "INTERNAL-v", "", true),
("PiksiMulti-starling-v1.8.0.bin", 1, 8, 0, "starling-v", "", true),
];

for (version_str, marketing, major, minor, namespace, dev, isdev) in success_test_cases {
let ver = SwiftVersion::parse_filename(version_str).unwrap();
assert_eq!(ver.marketing, marketing);
assert_eq!(ver.major, major);
assert_eq!(ver.minor, minor);
assert_eq!(ver.dev, dev);
assert_eq!(ver.namespace, namespace);
assert_eq!(ver.is_dev(), isdev);
}
}

#[test]
fn test_fail_cases() {
let fail_cases = [
"",
"alirjaliefjasef",
" ",
"asdf1234fdsa-v1.2.3",
];

for version_str in fail_cases {
let ver = SwiftVersion::parse(version_str);
assert!(ver.is_err(), "{:?}", version_str);
}
}

#[test]
fn test_eq() {
let expected_eq = [
("1.1.1", "1.1.1", true),
("1.1.1", "2.2.2", false),
("2.2.2", "1.1.1", false),
("2.2.2", "2.2.2", true),
("2.2.2", "2.2.2-dev", false),
("2.2.2-dev", "1.1.1", false),
("1.2.1", "1.1.1", false),
("1.1.2", "1.1.1", false),
("v1.1.1", "1.1.1", false),
];

for (first, second, equality) in expected_eq {
let lhs = SwiftVersion::parse(first).unwrap();
let rhs = SwiftVersion::parse(second).unwrap();
assert_eq!(
lhs == rhs,
equality,
"{:?} == {:?} != {:?}",
lhs,
rhs,
equality
);
}
}

#[test]
fn test_cmp() {
#[rustfmt::skip]
let expected_cmp = [
("1.1.1", "1.1.1", Some(Ordering::Equal)),
("1.1.2", "1.1.1", Some(Ordering::Greater)),
("1.2.0", "1.1.1", Some(Ordering::Greater)),
("2.1.0", "1.1.1", Some(Ordering::Greater)),
("2.1.0-dev", "1.1.1", None),
("PiksiMulti-v2.0.0.bin", "PiksiMulti-v3.0.0.bin", Some(Ordering::Less)),
("v1.0.0", "2.0.0", None),
("2.2.2", "2.2.2-dev", None),
];

for (first, second, cmp_result) in expected_cmp {
let lhs = SwiftVersion::parse(first).unwrap();
let rhs = SwiftVersion::parse(second).unwrap();
assert_eq!(lhs.partial_cmp(&rhs), cmp_result);
}
}
}
Loading