1#![forbid(unsafe_code)]
9
10use chrono::{DateTime, Utc};
11use std::fmt;
12use thiserror::Error;
13
14#[derive(Error, Debug)]
16pub enum CertifyError {
17 #[error("CSV parse error at line {line}: {message}")]
19 CsvParse {
20 line: usize,
22 message: String,
24 },
25
26 #[error("README marker not found: {0}")]
28 MarkerNotFound(String),
29
30 #[error("IO error: {0}")]
32 Io(#[from] std::io::Error),
33}
34
35pub type Result<T> = std::result::Result<T, CertifyError>;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum CertificationStatus {
41 Certified,
43 Provisional,
45 Blocked,
47 #[default]
49 Pending,
50}
51
52impl CertificationStatus {
53 #[must_use]
55 pub fn parse(s: &str) -> Self {
56 match s.to_uppercase().as_str() {
57 "CERTIFIED" => Self::Certified,
58 "PROVISIONAL" => Self::Provisional,
59 "BLOCKED" => Self::Blocked,
60 _ => Self::Pending,
61 }
62 }
63
64 #[must_use]
66 pub const fn badge(&self) -> &'static str {
67 match self {
68 Self::Certified => "",
69 Self::Provisional => "",
70 Self::Blocked => "",
71 Self::Pending => "",
72 }
73 }
74}
75
76impl fmt::Display for CertificationStatus {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Certified => write!(f, "CERTIFIED"),
80 Self::Provisional => write!(f, "PROVISIONAL"),
81 Self::Blocked => write!(f, "BLOCKED"),
82 Self::Pending => write!(f, "PENDING"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum SizeCategory {
90 Tiny,
92 #[default]
94 Small,
95 Medium,
97 Large,
99 XLarge,
101}
102
103impl SizeCategory {
104 #[must_use]
106 pub fn parse(s: &str) -> Self {
107 match s.to_lowercase().as_str() {
108 "tiny" => Self::Tiny,
109 "medium" => Self::Medium,
110 "large" => Self::Large,
111 "xlarge" => Self::XLarge,
112 _ => Self::Small,
114 }
115 }
116}
117
118#[allow(clippy::struct_excessive_bools)]
124#[derive(Debug, Clone)]
125pub struct ModelCertification {
126 pub model_id: String,
128 pub family: String,
130 pub parameters: String,
132 pub size_category: SizeCategory,
134 pub status: CertificationStatus,
136 pub mqs_score: u32,
138 pub grade: String,
140 pub certified_tier: String,
142 pub last_certified: Option<DateTime<Utc>>,
144 pub g1: bool,
146 pub g2: bool,
148 pub g3: bool,
150 pub g4: bool,
152 pub tps_gguf_cpu: Option<f64>,
154 pub tps_gguf_gpu: Option<f64>,
156 pub tps_apr_cpu: Option<f64>,
158 pub tps_apr_gpu: Option<f64>,
160 pub tps_st_cpu: Option<f64>,
162 pub tps_st_gpu: Option<f64>,
164 pub provenance_verified: bool,
166}
167
168impl ModelCertification {
169 #[must_use]
171 pub fn short_name(&self) -> &str {
172 self.model_id
173 .split('/')
174 .next_back()
175 .unwrap_or(&self.model_id)
176 }
177
178 #[must_use]
180 pub fn hf_url(&self) -> String {
181 format!("https://huggingface.co/{}", self.model_id)
182 }
183
184 #[must_use]
186 pub fn markdown_link(&self) -> String {
187 format!("[{}]({})", self.short_name(), self.hf_url())
188 }
189
190 #[must_use]
192 pub const fn gateway_symbol(passed: bool, status: CertificationStatus) -> &'static str {
193 if matches!(status, CertificationStatus::Pending) {
194 "-"
195 } else if passed {
196 "\u{2713}" } else {
198 "\u{2717}" }
200 }
201
202 #[must_use]
204 pub fn param_count(&self) -> f64 {
205 self.parameters
206 .trim_end_matches('B')
207 .parse::<f64>()
208 .unwrap_or(0.0)
209 }
210}
211
212#[allow(clippy::similar_names)]
218pub fn parse_csv(content: &str) -> Result<Vec<ModelCertification>> {
219 let mut models = Vec::new();
220 let mut lines = content.lines().enumerate();
221
222 let Some((_, header)) = lines.next() else {
224 return Ok(models);
225 };
226
227 let header_fields: Vec<&str> = header.split(',').collect();
229 if header_fields.len() < 13 {
230 return Err(CertifyError::CsvParse {
231 line: 1,
232 message: format!("expected at least 13 fields, got {}", header_fields.len()),
233 });
234 }
235
236 for (line_num, line) in lines {
237 if line.trim().is_empty() {
238 continue;
239 }
240
241 let fields: Vec<&str> = line.split(',').collect();
242 if fields.len() < 13 {
243 return Err(CertifyError::CsvParse {
244 line: line_num + 1,
245 message: format!("expected at least 13 fields, got {}", fields.len()),
246 });
247 }
248
249 let last_certified = DateTime::parse_from_rfc3339(fields[8])
250 .ok()
251 .map(|dt| dt.with_timezone(&Utc));
252
253 let tps_gguf_cpu = fields.get(13).and_then(|s| s.parse().ok());
255 let tps_gguf_gpu = fields.get(14).and_then(|s| s.parse().ok());
256 let tps_apr_cpu = fields.get(15).and_then(|s| s.parse().ok());
257 let tps_apr_gpu = fields.get(16).and_then(|s| s.parse().ok());
258 let tps_st_cpu = fields.get(17).and_then(|s| s.parse().ok());
259 let tps_st_gpu = fields.get(18).and_then(|s| s.parse().ok());
260 let provenance_verified = fields.get(19).is_some_and(|s| s.to_lowercase() == "true");
261
262 models.push(ModelCertification {
263 model_id: fields[0].to_string(),
264 family: fields[1].to_string(),
265 parameters: fields[2].to_string(),
266 size_category: SizeCategory::parse(fields[3]),
267 status: CertificationStatus::parse(fields[4]),
268 mqs_score: fields[5].parse().unwrap_or(0),
269 grade: fields[6].to_string(),
270 certified_tier: fields[7].to_string(),
271 last_certified,
272 g1: fields[9].to_lowercase() == "true",
273 g2: fields[10].to_lowercase() == "true",
274 g3: fields[11].to_lowercase() == "true",
275 g4: fields[12].to_lowercase() == "true",
276 tps_gguf_cpu,
277 tps_gguf_gpu,
278 tps_apr_cpu,
279 tps_apr_gpu,
280 tps_st_cpu,
281 tps_st_gpu,
282 provenance_verified,
283 });
284 }
285
286 Ok(models)
287}
288
289#[must_use]
291pub fn generate_summary(models: &[ModelCertification], timestamp: &str) -> String {
292 let total = models.len();
293 let certified = models
294 .iter()
295 .filter(|m| matches!(m.status, CertificationStatus::Certified))
296 .count();
297 let provisional = models
298 .iter()
299 .filter(|m| matches!(m.status, CertificationStatus::Provisional))
300 .count();
301 let blocked = models
302 .iter()
303 .filter(|m| matches!(m.status, CertificationStatus::Blocked))
304 .count();
305 let pending = models
306 .iter()
307 .filter(|m| matches!(m.status, CertificationStatus::Pending))
308 .count();
309
310 format!(
311 r"**Certification Summary** (updated: {timestamp})
312
313| Status | Count |
314|--------|-------|
315| Certified | {certified}/{total} |
316| Provisional | {provisional}/{total} |
317| Blocked | {blocked}/{total} |
318| Pending | {pending}/{total} |
319
320**Priority Family:** Qwen Coder (see [Certified Testing Spec](docs/specifications/certified-testing.md))"
321 )
322}
323
324#[must_use]
326pub fn generate_table(models: &[ModelCertification]) -> String {
327 let mut lines = Vec::new();
328
329 lines.push(
331 "| Model | Family | Size | Status | MQS | Grade | G1-4 | Prov | GGUF CPU | GGUF GPU | APR CPU | APR GPU | ST CPU | ST GPU |"
332 .to_string(),
333 );
334 lines.push(
335 "|-------|--------|------|--------|-----|-------|------|------|----------|----------|---------|---------|--------|--------|"
336 .to_string(),
337 );
338
339 let mut sorted: Vec<_> = models.iter().collect();
341 sorted.sort_by(|a, b| {
342 a.family.cmp(&b.family).then_with(|| {
343 a.param_count()
344 .partial_cmp(&b.param_count())
345 .unwrap_or(std::cmp::Ordering::Equal)
346 })
347 });
348
349 for m in sorted {
350 let gateways = if matches!(m.status, CertificationStatus::Pending) {
352 "-".to_string()
353 } else if m.g1 && m.g2 && m.g3 && m.g4 {
354 "\u{2713}".to_string() } else {
356 "\u{2717}".to_string() };
358
359 let prov = if matches!(m.status, CertificationStatus::Pending) {
361 "-"
362 } else if m.provenance_verified {
363 "\u{2713}" } else {
365 "\u{2717}" };
367
368 let fmt = |v: Option<f64>| v.map_or_else(|| "-".to_string(), |x| format!("{x:.1}"));
370
371 lines.push(format!(
372 "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |",
373 m.markdown_link(),
374 m.family,
375 m.parameters,
376 m.status.badge(),
377 m.mqs_score,
378 m.grade,
379 gateways,
380 prov,
381 fmt(m.tps_gguf_cpu),
382 fmt(m.tps_gguf_gpu),
383 fmt(m.tps_apr_cpu),
384 fmt(m.tps_apr_gpu),
385 fmt(m.tps_st_cpu),
386 fmt(m.tps_st_gpu),
387 ));
388 }
389
390 lines.join("\n")
391}
392
393pub const START_MARKER: &str = "<!-- CERTIFICATION_TABLE_START -->";
395pub const END_MARKER: &str = "<!-- CERTIFICATION_TABLE_END -->";
397
398#[must_use]
402pub fn write_csv(models: &[ModelCertification]) -> String {
403 let mut lines = Vec::new();
404
405 lines.push(
407 "model_id,family,parameters,size_category,status,mqs_score,grade,certified_tier,last_certified,g1,g2,g3,g4,tps_gguf_cpu,tps_gguf_gpu,tps_apr_cpu,tps_apr_gpu,tps_st_cpu,tps_st_gpu,provenance_verified"
408 .to_string(),
409 );
410
411 for m in models {
412 let size_cat = match m.size_category {
413 SizeCategory::Tiny => "tiny",
414 SizeCategory::Small => "small",
415 SizeCategory::Medium => "medium",
416 SizeCategory::Large => "large",
417 SizeCategory::XLarge => "xlarge",
418 };
419 let last_cert = m
420 .last_certified
421 .map_or_else(|| "2026-01-31T00:00:00Z".to_string(), |dt| dt.to_rfc3339());
422
423 let fmt = |v: Option<f64>| v.map_or(String::new(), |x| format!("{x:.1}"));
425
426 lines.push(format!(
427 "{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
428 m.model_id,
429 m.family,
430 m.parameters,
431 size_cat,
432 m.status,
433 m.mqs_score,
434 m.grade,
435 m.certified_tier,
436 last_cert,
437 m.g1,
438 m.g2,
439 m.g3,
440 m.g4,
441 fmt(m.tps_gguf_cpu),
442 fmt(m.tps_gguf_gpu),
443 fmt(m.tps_apr_cpu),
444 fmt(m.tps_apr_gpu),
445 fmt(m.tps_st_cpu),
446 fmt(m.tps_st_gpu),
447 m.provenance_verified,
448 ));
449 }
450
451 lines.join("\n") + "\n"
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
456pub enum CertificationTier {
457 #[default]
460 Mvp,
461 Full,
464}
465
466pub const MVP_PASS_THRESHOLD: f64 = 0.90;
468
469pub const MVP_PASS_SCORE: u32 = 800;
471
472pub const FULL_PASS_THRESHOLD: f64 = 0.95;
474
475pub const FULL_PASS_SCORE: u32 = 950;
477
478#[must_use]
480pub const fn status_from_score(mqs_score: u32, has_p0_failure: bool) -> CertificationStatus {
481 if has_p0_failure {
482 CertificationStatus::Blocked
483 } else if mqs_score >= 850 {
484 CertificationStatus::Certified
485 } else if mqs_score >= 700 {
486 CertificationStatus::Provisional
487 } else {
488 CertificationStatus::Blocked
489 }
490}
491
492#[must_use]
499pub fn status_from_tier(
500 tier: CertificationTier,
501 pass_rate: f64,
502 has_p0_failure: bool,
503) -> CertificationStatus {
504 if has_p0_failure {
505 return CertificationStatus::Blocked;
506 }
507
508 match tier {
509 CertificationTier::Mvp => {
510 if pass_rate >= MVP_PASS_THRESHOLD {
511 CertificationStatus::Provisional
512 } else {
513 CertificationStatus::Blocked
514 }
515 }
516 CertificationTier::Full => {
517 if pass_rate >= FULL_PASS_THRESHOLD {
518 CertificationStatus::Certified
519 } else if pass_rate >= MVP_PASS_THRESHOLD {
520 CertificationStatus::Provisional
521 } else {
522 CertificationStatus::Blocked
523 }
524 }
525 }
526}
527
528#[inline]
530fn scale_to_f_grade(pass_rate: f64) -> u32 {
531 let clamped = pass_rate.clamp(0.0, 1.0);
533 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
534 let score = (clamped * 699.0) as u32;
535 score.min(699)
536}
537
538#[must_use]
545pub fn score_from_tier(tier: CertificationTier, pass_rate: f64, has_p0_failure: bool) -> u32 {
546 if has_p0_failure {
547 return scale_to_f_grade(pass_rate);
549 }
550
551 match tier {
552 CertificationTier::Mvp => {
553 if pass_rate >= MVP_PASS_THRESHOLD {
554 MVP_PASS_SCORE
556 } else {
557 scale_to_f_grade(pass_rate)
559 }
560 }
561 CertificationTier::Full => {
562 if pass_rate >= FULL_PASS_THRESHOLD {
563 let bonus = ((pass_rate - FULL_PASS_THRESHOLD) * 1000.0).clamp(0.0, 50.0);
565 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
566 let bonus_u32 = bonus as u32;
567 FULL_PASS_SCORE + bonus_u32
568 } else if pass_rate >= MVP_PASS_THRESHOLD {
569 let ratio =
571 (pass_rate - MVP_PASS_THRESHOLD) / (FULL_PASS_THRESHOLD - MVP_PASS_THRESHOLD);
572 let bonus = (ratio * 99.0).clamp(0.0, 99.0);
573 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
574 let bonus_u32 = bonus as u32;
575 800 + bonus_u32
576 } else {
577 scale_to_f_grade(pass_rate)
579 }
580 }
581 }
582}
583
584#[must_use]
586pub const fn grade_from_score(mqs_score: u32) -> &'static str {
587 match mqs_score {
588 950..=1000 => "A+",
589 900..=949 => "A",
590 850..=899 => "B+",
591 800..=849 => "B",
592 700..=799 => "C",
593 _ => "F",
594 }
595}
596
597#[must_use]
604pub fn grade_from_tier(
605 tier: CertificationTier,
606 pass_rate: f64,
607 has_p0_failure: bool,
608) -> &'static str {
609 let score = score_from_tier(tier, pass_rate, has_p0_failure);
610 grade_from_score(score)
611}
612
613pub fn update_readme(readme: &str, table_content: &str) -> Result<String> {
619 let start_idx = readme
620 .find(START_MARKER)
621 .ok_or_else(|| CertifyError::MarkerNotFound(START_MARKER.to_string()))?;
622 let end_idx = readme
623 .find(END_MARKER)
624 .ok_or_else(|| CertifyError::MarkerNotFound(END_MARKER.to_string()))?;
625
626 let before = &readme[..start_idx + START_MARKER.len()];
627 let after = &readme[end_idx..];
628
629 Ok(format!("{before}\n{table_content}\n{after}"))
630}
631
632#[cfg(test)]
633#[allow(clippy::expect_used)]
634mod tests {
635 use super::*;
636
637 const SAMPLE_CSV: &str = r"model_id,family,parameters,size_category,status,mqs_score,grade,certified_tier,last_certified,g1,g2,g3,g4,tps_gguf_cpu,tps_gguf_gpu,tps_apr_cpu,tps_apr_gpu,tps_st_cpu,tps_st_gpu,provenance_verified
638Qwen/Qwen2.5-Coder-0.5B-Instruct,qwen-coder,0.5B,tiny,PENDING,0,-,none,2026-01-31T00:00:00Z,false,false,false,false,,,,,,false,false
639Qwen/Qwen2.5-Coder-1.5B-Instruct,qwen-coder,1.5B,small,CERTIFIED,920,A,deep,2026-01-31T12:00:00Z,true,true,true,true,25.5,85.2,22.3,78.1,18.1,62.5,true
640meta-llama/Llama-3.2-1B-Instruct,llama,1B,small,BLOCKED,450,F,smoke,2026-01-31T00:00:00Z,true,false,false,false,12.0,45.0,,,,,false";
641
642 #[test]
643 fn test_parse_csv_valid() {
644 let models = parse_csv(SAMPLE_CSV).expect("should parse");
645 assert_eq!(models.len(), 3);
646
647 assert_eq!(models[0].model_id, "Qwen/Qwen2.5-Coder-0.5B-Instruct");
648 assert_eq!(models[0].family, "qwen-coder");
649 assert_eq!(models[0].parameters, "0.5B");
650 assert!(matches!(models[0].status, CertificationStatus::Pending));
651
652 assert_eq!(models[1].mqs_score, 920);
653 assert!(matches!(models[1].status, CertificationStatus::Certified));
654 assert!(models[1].g1);
655 assert!(models[1].g2);
656
657 assert!(matches!(models[2].status, CertificationStatus::Blocked));
658 assert!(models[2].g1);
659 assert!(!models[2].g2);
660 }
661
662 #[test]
663 fn test_parse_csv_empty() {
664 let models = parse_csv("").expect("should parse empty");
665 assert!(models.is_empty());
666 }
667
668 #[test]
669 fn test_parse_csv_header_only() {
670 let csv = "model_id,family,parameters,size_category,status,mqs_score,grade,certified_tier,last_certified,g1,g2,g3,g4";
671 let models = parse_csv(csv).expect("should parse header only");
672 assert!(models.is_empty());
673 }
674
675 #[test]
676 fn test_parse_csv_invalid_fields() {
677 let csv = "a,b,c\n1,2,3";
678 let result = parse_csv(csv);
679 assert!(result.is_err());
680 }
681
682 #[test]
683 fn test_certification_status_parse() {
684 assert!(matches!(
685 CertificationStatus::parse("CERTIFIED"),
686 CertificationStatus::Certified
687 ));
688 assert!(matches!(
689 CertificationStatus::parse("certified"),
690 CertificationStatus::Certified
691 ));
692 assert!(matches!(
693 CertificationStatus::parse("PROVISIONAL"),
694 CertificationStatus::Provisional
695 ));
696 assert!(matches!(
697 CertificationStatus::parse("BLOCKED"),
698 CertificationStatus::Blocked
699 ));
700 assert!(matches!(
701 CertificationStatus::parse("PENDING"),
702 CertificationStatus::Pending
703 ));
704 assert!(matches!(
705 CertificationStatus::parse("unknown"),
706 CertificationStatus::Pending
707 ));
708 }
709
710 #[test]
711 fn test_certification_status_badge() {
712 assert!(
713 CertificationStatus::Certified
714 .badge()
715 .contains("brightgreen")
716 );
717 assert!(CertificationStatus::Provisional.badge().contains("yellow"));
718 assert!(CertificationStatus::Blocked.badge().contains("red"));
719 assert!(CertificationStatus::Pending.badge().contains("lightgray"));
720 }
721
722 #[test]
723 fn test_model_short_name() {
724 let model = ModelCertification {
725 model_id: "Qwen/Qwen2.5-Coder-1.5B-Instruct".to_string(),
726 family: "qwen-coder".to_string(),
727 parameters: "1.5B".to_string(),
728 size_category: SizeCategory::Small,
729 status: CertificationStatus::Pending,
730 mqs_score: 0,
731 grade: "-".to_string(),
732 certified_tier: "none".to_string(),
733 last_certified: None,
734 g1: false,
735 g2: false,
736 g3: false,
737 g4: false,
738 tps_gguf_cpu: None,
739 tps_gguf_gpu: None,
740 tps_apr_cpu: None,
741 tps_apr_gpu: None,
742 tps_st_cpu: None,
743 tps_st_gpu: None,
744 provenance_verified: false,
745 };
746 assert_eq!(model.short_name(), "Qwen2.5-Coder-1.5B-Instruct");
747 }
748
749 #[test]
750 fn test_model_hf_url() {
751 let model = ModelCertification {
752 model_id: "Qwen/Qwen2.5-Coder-1.5B-Instruct".to_string(),
753 family: String::new(),
754 parameters: String::new(),
755 size_category: SizeCategory::Small,
756 status: CertificationStatus::Pending,
757 mqs_score: 0,
758 grade: String::new(),
759 certified_tier: String::new(),
760 last_certified: None,
761 g1: false,
762 g2: false,
763 g3: false,
764 g4: false,
765 tps_gguf_cpu: None,
766 tps_gguf_gpu: None,
767 tps_apr_cpu: None,
768 tps_apr_gpu: None,
769 tps_st_cpu: None,
770 tps_st_gpu: None,
771 provenance_verified: false,
772 };
773 assert_eq!(
774 model.hf_url(),
775 "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct"
776 );
777 }
778
779 #[test]
780 fn test_model_param_count() {
781 let mut model = ModelCertification {
782 model_id: String::new(),
783 family: String::new(),
784 parameters: "1.5B".to_string(),
785 size_category: SizeCategory::Small,
786 status: CertificationStatus::Pending,
787 mqs_score: 0,
788 grade: String::new(),
789 certified_tier: String::new(),
790 last_certified: None,
791 g1: false,
792 g2: false,
793 g3: false,
794 g4: false,
795 tps_gguf_cpu: None,
796 tps_gguf_gpu: None,
797 tps_apr_cpu: None,
798 tps_apr_gpu: None,
799 tps_st_cpu: None,
800 tps_st_gpu: None,
801 provenance_verified: false,
802 };
803 assert!((model.param_count() - 1.5).abs() < f64::EPSILON);
804
805 model.parameters = "32B".to_string();
806 assert!((model.param_count() - 32.0).abs() < f64::EPSILON);
807
808 model.parameters = "invalid".to_string();
809 assert!((model.param_count() - 0.0).abs() < f64::EPSILON);
810 }
811
812 #[test]
813 fn test_gateway_symbol() {
814 assert_eq!(
815 ModelCertification::gateway_symbol(true, CertificationStatus::Certified),
816 "\u{2713}"
817 );
818 assert_eq!(
819 ModelCertification::gateway_symbol(false, CertificationStatus::Certified),
820 "\u{2717}"
821 );
822 assert_eq!(
823 ModelCertification::gateway_symbol(true, CertificationStatus::Pending),
824 "-"
825 );
826 assert_eq!(
827 ModelCertification::gateway_symbol(false, CertificationStatus::Pending),
828 "-"
829 );
830 }
831
832 #[test]
833 fn test_generate_summary() {
834 let models = parse_csv(SAMPLE_CSV).expect("should parse");
835 let summary = generate_summary(&models, "2026-01-31 12:00 UTC");
836
837 assert!(summary.contains("Certified | 1/3"));
838 assert!(summary.contains("Blocked | 1/3"));
839 assert!(summary.contains("Pending | 1/3"));
840 assert!(summary.contains("2026-01-31 12:00 UTC"));
841 }
842
843 #[test]
844 fn test_generate_table() {
845 let models = parse_csv(SAMPLE_CSV).expect("should parse");
846 let table = generate_table(&models);
847
848 assert!(table.contains("| Model | Family |"));
849 assert!(table.contains("Qwen2.5-Coder-0.5B-Instruct"));
850 assert!(table.contains("qwen-coder"));
851 assert!(table.contains("CERTIFIED-brightgreen"));
852 assert!(table.contains("BLOCKED-red"));
853 }
854
855 #[test]
856 fn test_generate_table_sorting() {
857 let models = parse_csv(SAMPLE_CSV).expect("should parse");
858 let table = generate_table(&models);
859 let lines: Vec<&str> = table.lines().collect();
860
861 let llama_idx = lines
864 .iter()
865 .position(|l| l.contains("Llama"))
866 .expect("llama found");
867 let qwen_05_idx = lines
868 .iter()
869 .position(|l| l.contains("0.5B"))
870 .expect("qwen 0.5 found");
871
872 assert!(
873 llama_idx < qwen_05_idx,
874 "llama should come before qwen-coder"
875 );
876 }
877
878 #[test]
879 fn test_update_readme_success() {
880 let readme = r"# Title
881
882Some content
883
884<!-- CERTIFICATION_TABLE_START -->
885old table
886<!-- CERTIFICATION_TABLE_END -->
887
888More content";
889
890 let new_table = "new table content";
891 let result = update_readme(readme, new_table).expect("should update");
892
893 assert!(result.contains("new table content"));
894 assert!(!result.contains("old table"));
895 assert!(result.contains("# Title"));
896 assert!(result.contains("More content"));
897 }
898
899 #[test]
900 fn test_update_readme_missing_start_marker() {
901 let readme = "no markers here <!-- CERTIFICATION_TABLE_END -->";
902 let result = update_readme(readme, "table");
903 assert!(matches!(result, Err(CertifyError::MarkerNotFound(_))));
904 }
905
906 #[test]
907 fn test_update_readme_missing_end_marker() {
908 let readme = "<!-- CERTIFICATION_TABLE_START --> no end marker";
909 let result = update_readme(readme, "table");
910 assert!(matches!(result, Err(CertifyError::MarkerNotFound(_))));
911 }
912
913 #[test]
914 fn test_size_category_parse() {
915 assert!(matches!(SizeCategory::parse("tiny"), SizeCategory::Tiny));
916 assert!(matches!(SizeCategory::parse("SMALL"), SizeCategory::Small));
917 assert!(matches!(
918 SizeCategory::parse("Medium"),
919 SizeCategory::Medium
920 ));
921 assert!(matches!(SizeCategory::parse("large"), SizeCategory::Large));
922 assert!(matches!(
923 SizeCategory::parse("xlarge"),
924 SizeCategory::XLarge
925 ));
926 assert!(matches!(
927 SizeCategory::parse("unknown"),
928 SizeCategory::Small
929 ));
930 }
931
932 #[test]
933 fn test_certification_status_display() {
934 assert_eq!(format!("{}", CertificationStatus::Certified), "CERTIFIED");
935 assert_eq!(
936 format!("{}", CertificationStatus::Provisional),
937 "PROVISIONAL"
938 );
939 assert_eq!(format!("{}", CertificationStatus::Blocked), "BLOCKED");
940 assert_eq!(format!("{}", CertificationStatus::Pending), "PENDING");
941 }
942
943 #[test]
944 fn test_short_name_no_slash() {
945 let model = ModelCertification {
946 model_id: "model-without-org".to_string(),
947 family: String::new(),
948 parameters: String::new(),
949 size_category: SizeCategory::Small,
950 status: CertificationStatus::Pending,
951 mqs_score: 0,
952 grade: String::new(),
953 certified_tier: String::new(),
954 last_certified: None,
955 g1: false,
956 g2: false,
957 g3: false,
958 g4: false,
959 tps_gguf_cpu: None,
960 tps_gguf_gpu: None,
961 tps_apr_cpu: None,
962 tps_apr_gpu: None,
963 tps_st_cpu: None,
964 tps_st_gpu: None,
965 provenance_verified: false,
966 };
967 assert_eq!(model.short_name(), "model-without-org");
968 }
969
970 #[test]
971 fn test_markdown_link() {
972 let model = ModelCertification {
973 model_id: "Org/Model".to_string(),
974 family: String::new(),
975 parameters: String::new(),
976 size_category: SizeCategory::Small,
977 status: CertificationStatus::Pending,
978 mqs_score: 0,
979 grade: String::new(),
980 certified_tier: String::new(),
981 last_certified: None,
982 g1: false,
983 g2: false,
984 g3: false,
985 g4: false,
986 tps_gguf_cpu: None,
987 tps_gguf_gpu: None,
988 tps_apr_cpu: None,
989 tps_apr_gpu: None,
990 tps_st_cpu: None,
991 tps_st_gpu: None,
992 provenance_verified: false,
993 };
994 assert_eq!(
995 model.markdown_link(),
996 "[Model](https://huggingface.co/Org/Model)"
997 );
998 }
999
1000 #[test]
1001 fn test_write_csv_roundtrip() {
1002 let models = parse_csv(SAMPLE_CSV).expect("should parse");
1003 let csv_output = write_csv(&models);
1004
1005 let reparsed = parse_csv(&csv_output).expect("should reparse");
1007 assert_eq!(reparsed.len(), models.len());
1008
1009 assert_eq!(reparsed[0].model_id, models[0].model_id);
1011 assert_eq!(reparsed[0].family, models[0].family);
1012 assert_eq!(reparsed[0].mqs_score, models[0].mqs_score);
1013 }
1014
1015 #[test]
1016 fn test_write_csv_has_header() {
1017 let models = parse_csv(SAMPLE_CSV).expect("should parse");
1018 let csv_output = write_csv(&models);
1019 assert!(csv_output.starts_with("model_id,family,"));
1020 }
1021
1022 #[test]
1023 fn test_status_from_score_certified() {
1024 assert!(matches!(
1025 status_from_score(900_u32, false),
1026 CertificationStatus::Certified
1027 ));
1028 assert!(matches!(
1029 status_from_score(850_u32, false),
1030 CertificationStatus::Certified
1031 ));
1032 }
1033
1034 #[test]
1035 fn test_status_from_score_provisional() {
1036 assert!(matches!(
1037 status_from_score(800_u32, false),
1038 CertificationStatus::Provisional
1039 ));
1040 assert!(matches!(
1041 status_from_score(700_u32, false),
1042 CertificationStatus::Provisional
1043 ));
1044 }
1045
1046 #[test]
1047 fn test_status_from_score_blocked() {
1048 assert!(matches!(
1049 status_from_score(699_u32, false),
1050 CertificationStatus::Blocked
1051 ));
1052 assert!(matches!(
1053 status_from_score(0_u32, false),
1054 CertificationStatus::Blocked
1055 ));
1056 }
1057
1058 #[test]
1059 fn test_status_from_score_p0_failure() {
1060 assert!(matches!(
1062 status_from_score(950_u32, true),
1063 CertificationStatus::Blocked
1064 ));
1065 assert!(matches!(
1066 status_from_score(900_u32, true),
1067 CertificationStatus::Blocked
1068 ));
1069 }
1070
1071 #[test]
1072 fn test_grade_from_score() {
1073 assert_eq!(grade_from_score(1000_u32), "A+");
1074 assert_eq!(grade_from_score(950_u32), "A+");
1075 assert_eq!(grade_from_score(920_u32), "A");
1076 assert_eq!(grade_from_score(900_u32), "A");
1077 assert_eq!(grade_from_score(880_u32), "B+");
1078 assert_eq!(grade_from_score(850_u32), "B+");
1079 assert_eq!(grade_from_score(820_u32), "B");
1080 assert_eq!(grade_from_score(800_u32), "B");
1081 assert_eq!(grade_from_score(750_u32), "C");
1082 assert_eq!(grade_from_score(700_u32), "C");
1083 assert_eq!(grade_from_score(699_u32), "F");
1084 assert_eq!(grade_from_score(0_u32), "F");
1085 }
1086
1087 #[test]
1088 fn test_mvp_tier_pass() {
1089 let status = status_from_tier(CertificationTier::Mvp, 0.95, false);
1091 assert!(matches!(status, CertificationStatus::Provisional));
1092
1093 let score = score_from_tier(CertificationTier::Mvp, 0.95, false);
1094 assert_eq!(score, 800);
1095
1096 let grade = grade_from_tier(CertificationTier::Mvp, 0.95, false);
1097 assert_eq!(grade, "B");
1098 }
1099
1100 #[test]
1101 fn test_mvp_tier_exactly_90_percent() {
1102 let status = status_from_tier(CertificationTier::Mvp, 0.90, false);
1104 assert!(matches!(status, CertificationStatus::Provisional));
1105
1106 let score = score_from_tier(CertificationTier::Mvp, 0.90, false);
1107 assert_eq!(score, 800);
1108 }
1109
1110 #[test]
1111 fn test_mvp_tier_fail() {
1112 let status = status_from_tier(CertificationTier::Mvp, 0.85, false);
1114 assert!(matches!(status, CertificationStatus::Blocked));
1115
1116 let score = score_from_tier(CertificationTier::Mvp, 0.85, false);
1117 assert!(score < 700); }
1119
1120 #[test]
1121 fn test_mvp_tier_p0_failure() {
1122 let status = status_from_tier(CertificationTier::Mvp, 0.99, true);
1124 assert!(matches!(status, CertificationStatus::Blocked));
1125
1126 let score = score_from_tier(CertificationTier::Mvp, 0.99, true);
1127 assert!(score < 700); }
1129
1130 #[test]
1131 fn test_full_tier_pass() {
1132 let status = status_from_tier(CertificationTier::Full, 0.98, false);
1134 assert!(matches!(status, CertificationStatus::Certified));
1135
1136 let score = score_from_tier(CertificationTier::Full, 0.98, false);
1137 assert!(score >= 950);
1138
1139 let grade = grade_from_tier(CertificationTier::Full, 0.98, false);
1140 assert_eq!(grade, "A+");
1141 }
1142
1143 #[test]
1144 fn test_full_tier_provisional() {
1145 let status = status_from_tier(CertificationTier::Full, 0.92, false);
1147 assert!(matches!(status, CertificationStatus::Provisional));
1148
1149 let score = score_from_tier(CertificationTier::Full, 0.92, false);
1150 assert!((800..900).contains(&score)); }
1152
1153 #[test]
1154 fn test_full_tier_fail() {
1155 let status = status_from_tier(CertificationTier::Full, 0.85, false);
1157 assert!(matches!(status, CertificationStatus::Blocked));
1158
1159 let score = score_from_tier(CertificationTier::Full, 0.85, false);
1160 assert!(score < 700);
1161 }
1162
1163 #[test]
1164 fn test_certification_tier_default() {
1165 let tier = CertificationTier::default();
1166 assert!(matches!(tier, CertificationTier::Mvp));
1167 }
1168
1169 #[test]
1170 fn test_parse_csv_with_empty_lines() {
1171 let csv = r"model_id,family,parameters,size_category,status,mqs_score,grade,certified_tier,last_certified,g1,g2,g3,g4
1172
1173Qwen/Qwen2.5-Coder-0.5B-Instruct,qwen-coder,0.5B,tiny,PENDING,0,-,none,2026-01-31T00:00:00Z,false,false,false,false
1174
1175";
1176 let models = parse_csv(csv).expect("should parse with empty lines");
1177 assert_eq!(models.len(), 1);
1178 assert_eq!(models[0].model_id, "Qwen/Qwen2.5-Coder-0.5B-Instruct");
1179 }
1180
1181 #[test]
1182 fn test_parse_csv_insufficient_fields_in_line() {
1183 let csv = r"model_id,family,parameters,size_category,status,mqs_score,grade,certified_tier,last_certified,g1,g2,g3,g4
1184only,a,few,fields";
1185 let result = parse_csv(csv);
1186 assert!(result.is_err());
1187 let err = result.expect_err("Should be an error");
1188 assert!(
1189 matches!(err, CertifyError::CsvParse { line: 2, .. }),
1190 "Error should indicate line 2"
1191 );
1192 }
1193
1194 #[test]
1195 fn test_write_csv_all_size_categories() {
1196 let models = vec![
1198 ModelCertification {
1199 model_id: "tiny-model".to_string(),
1200 family: "test".to_string(),
1201 parameters: "0.5B".to_string(),
1202 size_category: SizeCategory::Tiny,
1203 status: CertificationStatus::Pending,
1204 mqs_score: 0,
1205 grade: "-".to_string(),
1206 certified_tier: "none".to_string(),
1207 last_certified: None,
1208 g1: false,
1209 g2: false,
1210 g3: false,
1211 g4: false,
1212 tps_gguf_cpu: None,
1213 tps_gguf_gpu: None,
1214 tps_apr_cpu: None,
1215 tps_apr_gpu: None,
1216 tps_st_cpu: None,
1217 tps_st_gpu: None,
1218 provenance_verified: false,
1219 },
1220 ModelCertification {
1221 model_id: "medium-model".to_string(),
1222 family: "test".to_string(),
1223 parameters: "7B".to_string(),
1224 size_category: SizeCategory::Medium,
1225 status: CertificationStatus::Pending,
1226 mqs_score: 0,
1227 grade: "-".to_string(),
1228 certified_tier: "none".to_string(),
1229 last_certified: None,
1230 g1: false,
1231 g2: false,
1232 g3: false,
1233 g4: false,
1234 tps_gguf_cpu: None,
1235 tps_gguf_gpu: None,
1236 tps_apr_cpu: None,
1237 tps_apr_gpu: None,
1238 tps_st_cpu: None,
1239 tps_st_gpu: None,
1240 provenance_verified: false,
1241 },
1242 ModelCertification {
1243 model_id: "large-model".to_string(),
1244 family: "test".to_string(),
1245 parameters: "34B".to_string(),
1246 size_category: SizeCategory::Large,
1247 status: CertificationStatus::Pending,
1248 mqs_score: 0,
1249 grade: "-".to_string(),
1250 certified_tier: "none".to_string(),
1251 last_certified: None,
1252 g1: false,
1253 g2: false,
1254 g3: false,
1255 g4: false,
1256 tps_gguf_cpu: None,
1257 tps_gguf_gpu: None,
1258 tps_apr_cpu: None,
1259 tps_apr_gpu: None,
1260 tps_st_cpu: None,
1261 tps_st_gpu: None,
1262 provenance_verified: false,
1263 },
1264 ModelCertification {
1265 model_id: "xlarge-model".to_string(),
1266 family: "test".to_string(),
1267 parameters: "70B".to_string(),
1268 size_category: SizeCategory::XLarge,
1269 status: CertificationStatus::Pending,
1270 mqs_score: 0,
1271 grade: "-".to_string(),
1272 certified_tier: "none".to_string(),
1273 last_certified: None,
1274 g1: false,
1275 g2: false,
1276 g3: false,
1277 g4: false,
1278 tps_gguf_cpu: None,
1279 tps_gguf_gpu: None,
1280 tps_apr_cpu: None,
1281 tps_apr_gpu: None,
1282 tps_st_cpu: None,
1283 tps_st_gpu: None,
1284 provenance_verified: false,
1285 },
1286 ];
1287
1288 let csv_output = write_csv(&models);
1289 assert!(csv_output.contains(",tiny,"));
1290 assert!(csv_output.contains(",medium,"));
1291 assert!(csv_output.contains(",large,"));
1292 assert!(csv_output.contains(",xlarge,"));
1293 }
1294}