Skip to content

Commit 6bae1ec

Browse files
committed
feat(tests): implement initial unit tests
1 parent 089740b commit 6bae1ec

35 files changed

Lines changed: 2402 additions & 51 deletions

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,45 @@ jobs:
6464
uses: Swatinem/rust-cache@v2
6565
- name: Generate coverage
6666
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
67+
- name: Generate coverage JSON
68+
run: cargo llvm-cov --all-features --workspace --json --output-path coverage.json
69+
- name: Extract coverage percentage
70+
id: coverage
71+
run: |
72+
COVERAGE=$(cargo llvm-cov --all-features --workspace 2>&1 | grep "TOTAL" | awk '{print $NF}' | tr -d '%')
73+
echo "percentage=${COVERAGE}" >> $GITHUB_OUTPUT
74+
echo "Coverage: ${COVERAGE}%"
75+
76+
# Create badge JSON for shields.io endpoint
77+
COLOR="red"
78+
if (( $(echo "$COVERAGE >= 90" | bc -l) )); then
79+
COLOR="brightgreen"
80+
elif (( $(echo "$COVERAGE >= 80" | bc -l) )); then
81+
COLOR="green"
82+
elif (( $(echo "$COVERAGE >= 70" | bc -l) )); then
83+
COLOR="yellowgreen"
84+
elif (( $(echo "$COVERAGE >= 60" | bc -l) )); then
85+
COLOR="yellow"
86+
elif (( $(echo "$COVERAGE >= 50" | bc -l) )); then
87+
COLOR="orange"
88+
fi
89+
90+
echo "{\"schemaVersion\":1,\"label\":\"coverage\",\"message\":\"${COVERAGE}%\",\"color\":\"${COLOR}\"}" > coverage-badge.json
6791
- name: Upload coverage to Codecov
6892
uses: codecov/codecov-action@v4
6993
with:
7094
files: lcov.info
7195
fail_ci_if_error: false
96+
- name: Upload coverage badge artifact
97+
uses: actions/upload-artifact@v4
98+
with:
99+
name: coverage-badge
100+
path: coverage-badge.json
101+
- name: Upload coverage report
102+
uses: actions/upload-artifact@v4
103+
with:
104+
name: coverage-report
105+
path: coverage.json
72106

73107
build:
74108
name: Build

crates/ev-core/src/domain/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod types;
1010
mod vehicle;
1111

1212
pub use battery::{Battery, Preconditioning, UsableSocWindow, Warranty};
13-
pub use body::{Body, Capacity, Dimensions, Weights};
13+
pub use body::{Body, Capacity, Dimensions, Performance, Weights};
1414
pub use charging::{
1515
ChargeCurve, ChargeCurvePoint, ChargePort, Charging, ChargingAc, ChargingDc, ChargingProtocols,
1616
ChargingTime, Conditions,
@@ -22,5 +22,5 @@ pub use sources::Source;
2222
pub use types::{SlugName, VehicleId, Year};
2323
pub use vehicle::{Vehicle, VehicleAvailability};
2424

25-
pub use charging::{V2LOutlet, V2G, V2H, V2L, V2X};
25+
pub use charging::{V2G, V2H, V2L, V2LOutlet, V2X};
2626
pub use metadata::{Msrp, Pricing, Software, WheelsTires};

crates/ev-core/src/domain/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use serde::{Deserialize, Serialize};
22

33
use crate::error::ValidationError;
4-
use crate::validation::{validate_slug, validate_year, Validate};
4+
use crate::validation::{Validate, validate_slug, validate_year};
55

66
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77
pub struct SlugName {

crates/ev-core/src/domain/vehicle.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,149 @@ mod tests {
309309
Err(ValidationError::MissingChargePort)
310310
));
311311
}
312+
313+
#[test]
314+
fn test_vehicle_missing_range() {
315+
let mut vehicle = create_test_vehicle();
316+
vehicle.range.rated = vec![];
317+
assert!(matches!(
318+
vehicle.validate(),
319+
Err(ValidationError::MissingRatedRange)
320+
));
321+
}
322+
323+
#[test]
324+
fn test_vehicle_missing_sources() {
325+
let mut vehicle = create_test_vehicle();
326+
vehicle.sources = vec![];
327+
assert!(matches!(
328+
vehicle.validate(),
329+
Err(ValidationError::MissingSource)
330+
));
331+
}
332+
333+
#[test]
334+
fn test_vehicle_multiple_validation_errors() {
335+
let mut vehicle = create_test_vehicle();
336+
vehicle.battery = Battery::default();
337+
vehicle.charge_ports = vec![];
338+
vehicle.range.rated = vec![];
339+
vehicle.sources = vec![];
340+
assert!(matches!(
341+
vehicle.validate(),
342+
Err(ValidationError::Multiple(_))
343+
));
344+
}
345+
346+
#[test]
347+
fn test_vehicle_is_variant_false() {
348+
let vehicle = create_test_vehicle();
349+
assert!(!vehicle.is_variant());
350+
}
351+
352+
#[test]
353+
fn test_vehicle_is_variant_true() {
354+
let mut vehicle = create_test_vehicle();
355+
vehicle.variant = Some(Variant {
356+
slug: "long_range".to_string(),
357+
name: "Long Range".to_string(),
358+
kind: None,
359+
notes: None,
360+
});
361+
assert!(vehicle.is_variant());
362+
}
363+
364+
#[test]
365+
fn test_vehicle_usable_battery_kwh() {
366+
let vehicle = create_test_vehicle();
367+
assert_eq!(vehicle.usable_battery_kwh(), Some(60.0));
368+
}
369+
370+
#[test]
371+
fn test_vehicle_usable_battery_kwh_none() {
372+
let mut vehicle = create_test_vehicle();
373+
vehicle.battery = Battery::default();
374+
assert_eq!(vehicle.usable_battery_kwh(), None);
375+
}
376+
377+
#[test]
378+
fn test_vehicle_wltp_range_km() {
379+
let vehicle = create_test_vehicle();
380+
assert_eq!(vehicle.wltp_range_km(), Some(513.0));
381+
}
382+
383+
#[test]
384+
fn test_vehicle_epa_range_km_none() {
385+
let vehicle = create_test_vehicle();
386+
assert_eq!(vehicle.epa_range_km(), None);
387+
}
388+
389+
#[test]
390+
fn test_vehicle_epa_range_km_some() {
391+
let mut vehicle = create_test_vehicle();
392+
vehicle.range.rated.push(RangeRated {
393+
cycle: RangeCycle::Epa,
394+
range_km: 400.0,
395+
notes: None,
396+
});
397+
assert_eq!(vehicle.epa_range_km(), Some(400.0));
398+
}
399+
400+
#[test]
401+
fn test_vehicle_max_dc_power_kw_none() {
402+
let vehicle = create_test_vehicle();
403+
assert_eq!(vehicle.max_dc_power_kw(), None);
404+
}
405+
406+
#[test]
407+
fn test_vehicle_max_dc_power_kw_some() {
408+
use crate::domain::charging::ChargingDc;
409+
let mut vehicle = create_test_vehicle();
410+
vehicle.charging.dc = Some(ChargingDc {
411+
max_power_kw: 250.0,
412+
voltage_range_v: None,
413+
max_current_a: None,
414+
architecture_voltage_class: None,
415+
power_limits_by_voltage: None,
416+
notes: None,
417+
});
418+
assert_eq!(vehicle.max_dc_power_kw(), Some(250.0));
419+
}
420+
421+
#[test]
422+
fn test_vehicle_max_ac_power_kw_none() {
423+
let vehicle = create_test_vehicle();
424+
assert_eq!(vehicle.max_ac_power_kw(), None);
425+
}
426+
427+
#[test]
428+
fn test_vehicle_max_ac_power_kw_some() {
429+
use crate::domain::charging::ChargingAc;
430+
let mut vehicle = create_test_vehicle();
431+
vehicle.charging.ac = Some(ChargingAc {
432+
max_power_kw: 11.0,
433+
supported_power_steps_kw: None,
434+
phases: None,
435+
voltage_range_v: None,
436+
frequency_hz: None,
437+
max_current_a: None,
438+
onboard_charger_count: None,
439+
notes: None,
440+
});
441+
assert_eq!(vehicle.max_ac_power_kw(), Some(11.0));
442+
}
443+
444+
#[test]
445+
fn test_vehicle_invalid_make_slug() {
446+
let mut vehicle = create_test_vehicle();
447+
vehicle.make.slug = "INVALID".to_string();
448+
assert!(vehicle.validate().is_err());
449+
}
450+
451+
#[test]
452+
fn test_vehicle_invalid_year() {
453+
let mut vehicle = create_test_vehicle();
454+
vehicle.year = 1800;
455+
assert!(vehicle.validate().is_err());
456+
}
312457
}

crates/ev-core/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ pub mod validation;
1111
pub use domain::{
1212
Battery, Body, Capacity, ChargeCurve, ChargeCurvePoint, ChargePort, Charging, ChargingAc,
1313
ChargingDc, ChargingProtocols, ChargingTime, Conditions, Dimensions, Efficiency, Images, Links,
14-
Metadata, Motor, Msrp, Powertrain, Preconditioning, Pricing, Range, RangeRated, RangeRealWorld,
15-
SlugName, Source, Transmission, UsableSocWindow, V2LOutlet, Variant, Vehicle,
16-
VehicleAvailability, VehicleId, Warranty, Weights, WheelsTires, Year, V2G, V2H, V2L, V2X,
14+
Metadata, Motor, Msrp, Performance, Powertrain, Preconditioning, Pricing, Range, RangeRated,
15+
RangeRealWorld, SlugName, Source, Transmission, UsableSocWindow, V2G, V2H, V2L, V2LOutlet, V2X,
16+
Variant, Vehicle, VehicleAvailability, VehicleId, Warranty, Weights, WheelsTires, Year,
1717
};
1818

1919
pub use domain::enums::{

crates/ev-core/src/validation.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,53 @@ mod tests {
131131
assert!(validate_currency_code("usd").is_err());
132132
assert!(validate_currency_code("US").is_err());
133133
}
134+
135+
#[test]
136+
fn test_validate_url_valid() {
137+
assert!(validate_url("https://example.com").is_ok());
138+
assert!(validate_url("http://example.com").is_ok());
139+
assert!(validate_url("https://example.com/path").is_ok());
140+
}
141+
142+
#[test]
143+
fn test_validate_url_invalid() {
144+
assert!(validate_url("").is_err());
145+
assert!(validate_url("example.com").is_err());
146+
assert!(validate_url("ftp://example.com").is_err());
147+
assert!(validate_url("www.example.com").is_err());
148+
}
149+
150+
#[test]
151+
fn test_collect_errors_empty() {
152+
let items: Vec<String> = vec![];
153+
let result = collect_errors(items, |_: &String| Ok(()));
154+
assert!(result.is_ok());
155+
}
156+
157+
#[test]
158+
fn test_collect_errors_all_valid() {
159+
let items = vec!["tesla", "byd", "rivian"];
160+
let result = collect_errors(items, |s: &&str| validate_slug(s));
161+
assert!(result.is_ok());
162+
}
163+
164+
#[test]
165+
fn test_collect_errors_single_error() {
166+
let items = vec!["tesla", "INVALID", "rivian"];
167+
let result = collect_errors(items, |s: &&str| validate_slug(s));
168+
assert!(result.is_err());
169+
if let Err(e) = result {
170+
assert!(matches!(e, ValidationError::InvalidSlug { .. }));
171+
}
172+
}
173+
174+
#[test]
175+
fn test_collect_errors_multiple_errors() {
176+
let items = vec!["INVALID1", "INVALID2", "INVALID3"];
177+
let result = collect_errors(items, |s: &&str| validate_slug(s));
178+
assert!(result.is_err());
179+
if let Err(e) = result {
180+
assert!(matches!(e, ValidationError::Multiple(_)));
181+
}
182+
}
134183
}

crates/ev-etl/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{Context, Result};
22
use clap::Parser;
33
use ev_etl::{cli::Cli, run_pipeline, run_validation};
4-
use tracing::{info, Level};
4+
use tracing::{Level, info};
55
use tracing_subscriber::FmtSubscriber;
66

77
fn main() -> Result<()> {

crates/ev-etl/src/merge/strategy.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,77 @@ mod tests {
9797
let result = deep_merge(&base, &overlay);
9898
assert_eq!(result, json!({"ports": ["nacs"]}));
9999
}
100+
101+
#[test]
102+
fn test_merge_arrays_replace() {
103+
let base = vec![json!("a"), json!("b")];
104+
let overlay = vec![json!("c")];
105+
let result = merge_arrays_replace(&base, &overlay);
106+
assert_eq!(result, vec![json!("c")]);
107+
}
108+
109+
#[test]
110+
fn test_merge_arrays_replace_empty_overlay() {
111+
let base = vec![json!("a"), json!("b")];
112+
let overlay: Vec<Value> = vec![];
113+
let result = merge_arrays_replace(&base, &overlay);
114+
assert!(result.is_empty());
115+
}
116+
117+
#[test]
118+
fn test_remove_null_values_simple() {
119+
let mut value = json!({"a": 1, "b": null, "c": 3});
120+
remove_null_values(&mut value);
121+
assert_eq!(value, json!({"a": 1, "c": 3}));
122+
}
123+
124+
#[test]
125+
fn test_remove_null_values_nested() {
126+
let mut value = json!({
127+
"a": 1,
128+
"nested": {
129+
"b": null,
130+
"c": 2
131+
}
132+
});
133+
remove_null_values(&mut value);
134+
assert_eq!(value, json!({"a": 1, "nested": {"c": 2}}));
135+
}
136+
137+
#[test]
138+
fn test_remove_null_values_array() {
139+
let mut value = json!([{"a": null}, {"b": 1}]);
140+
remove_null_values(&mut value);
141+
assert_eq!(value, json!([{}, {"b": 1}]));
142+
}
143+
144+
#[test]
145+
fn test_remove_null_values_no_nulls() {
146+
let mut value = json!({"a": 1, "b": 2});
147+
remove_null_values(&mut value);
148+
assert_eq!(value, json!({"a": 1, "b": 2}));
149+
}
150+
151+
#[test]
152+
fn test_remove_null_values_scalar() {
153+
let mut value = json!(42);
154+
remove_null_values(&mut value);
155+
assert_eq!(value, json!(42));
156+
}
157+
158+
#[test]
159+
fn test_deep_merge_scalar_override() {
160+
let base = json!({"value": 1});
161+
let overlay = json!({"value": "string"});
162+
let result = deep_merge(&base, &overlay);
163+
assert_eq!(result["value"], "string");
164+
}
165+
166+
#[test]
167+
fn test_deep_merge_non_object_base() {
168+
let base = json!("scalar");
169+
let overlay = json!({"a": 1});
170+
let result = deep_merge(&base, &overlay);
171+
assert_eq!(result, json!({"a": 1}));
172+
}
100173
}

0 commit comments

Comments
 (0)