use bwipp::{render_png, render_svg, Options, Symbology};
fn is_stub(_s: Symbology) -> bool {
false
}
#[test]
fn every_symbology_renders_svg() {
let opts = Options::default();
for &s in Symbology::all() {
if is_stub(s) {
continue;
}
let svg = render_svg(s, s.default_data(), &opts)
.unwrap_or_else(|e| panic!("{} svg failed: {e}", s.id()));
assert!(svg.starts_with("<svg"), "{} svg has wrong prefix", s.id());
assert!(svg.ends_with("</svg>\n"), "{} svg has wrong suffix", s.id());
assert!(
svg.contains("<rect "),
"{} svg contains no <rect> element",
s.id()
);
}
}
#[test]
fn every_symbology_renders_png() {
let opts = Options::default();
for &s in Symbology::all() {
if is_stub(s) {
continue;
}
let png = render_png(s, s.default_data(), &opts)
.unwrap_or_else(|e| panic!("{} png failed: {e}", s.id()));
assert!(
png.starts_with(&[0x89, b'P', b'N', b'G']),
"{} not a PNG",
s.id()
);
assert!(png.len() > 100, "{} png suspiciously small", s.id());
}
}
#[test]
fn id_round_trip() {
for &s in Symbology::all() {
assert_eq!(Symbology::from_id(s.id()), Some(s));
}
}
#[test]
fn renderer_options_change_svg_output() {
let baseline = render_svg(Symbology::Code39, "HELLO", &Options::default())
.expect("Code 39 baseline render with default Options must succeed");
let opts = Options {
foreground: [255, 0, 0],
..Options::default()
};
let red = render_svg(Symbology::Code39, "HELLO", &opts)
.expect("Code 39 render with custom foreground [255,0,0] must succeed");
assert!(
red.contains("#ff0000"),
"custom foreground colour not reflected in SVG"
);
assert_ne!(red, baseline);
let opts = Options {
background: [0, 255, 0],
..Options::default()
};
let green_bg = render_svg(Symbology::Code39, "HELLO", &opts)
.expect("Code 39 render with custom background [0,255,0] must succeed");
assert!(
green_bg.contains("#00ff00"),
"custom background colour not reflected in SVG"
);
let opts = Options {
scale: 8,
..Options::default()
};
let big = render_svg(Symbology::Code39, "HELLO", &opts)
.expect("Code 39 render with scale=8 must succeed");
assert!(big.len() >= baseline.len());
assert_ne!(big, baseline);
let opts = Options {
include_text: true,
..Options::default()
};
let with_text = render_svg(Symbology::Code39, "HELLO", &opts)
.expect("Code 39 render with include_text=true must succeed");
assert!(!baseline.contains("<text"), "baseline shouldn't have text");
assert!(with_text.contains("<text"), "include_text should add text");
assert!(
with_text.contains("HELLO"),
"the rendered text should appear in the SVG"
);
}
#[test]
fn alias_ids_route_to_canonical_symbology() {
let cases: &[(&str, Symbology)] = &[
("code128a", Symbology::Code128),
("code128b", Symbology::Code128),
("code128c", Symbology::Code128),
("plessey_bidir", Symbology::Plessey),
("swedish_postal", Symbology::Sscc18),
("qrcode_iso", Symbology::QrCode),
("qr_code", Symbology::QrCode),
("telepen_alpha", Symbology::TelepenNumeric),
("telepen_numeric", Symbology::TelepenNumeric),
("datamatrixrectangular", Symbology::DataMatrixRectangular),
("datamatrix_rectangular", Symbology::DataMatrixRectangular),
("usps_postnet5", Symbology::Postnet),
("usps_postnet9", Symbology::Postnet),
("usps_postnet11", Symbology::Postnet),
("planet12", Symbology::Planet),
("planet14", Symbology::Planet),
("usps_imb", Symbology::UspsOneCode),
("pzn", Symbology::Pzn7),
("auspost", Symbology::AuspostCustomer),
("rationalizedCodabar", Symbology::Codabar),
("rationalizedcodabar", Symbology::Codabar),
("ean13composite", Symbology::CompositeEan13Cca),
("ean8composite", Symbology::CompositeEan8Cca),
("upcacomposite", Symbology::CompositeUpcaCca),
("upcecomposite", Symbology::CompositeUpceCca),
("databaromnicomposite", Symbology::CompositeDatabarOmniCca),
(
"databarlimitedcomposite",
Symbology::CompositeDatabarLimitedCca,
),
(
"databarexpandedcomposite",
Symbology::CompositeDatabarExpandedCca,
),
("gs1-128composite", Symbology::CompositeGs1_128Cca),
("ean14", Symbology::Ean14),
("gtin14", Symbology::Ean14),
("hibcazteccode", Symbology::HibcAztecCode),
(
"hibcdatamatrixrectangular",
Symbology::HibcDataMatrixRectangular,
),
(
"gs1datamatrixrectangular",
Symbology::Gs1DataMatrixRectangular,
),
(
"gs1-datamatrix-rectangular",
Symbology::Gs1DataMatrixRectangular,
),
("azteccodecompact", Symbology::AztecCodeCompact),
("aztec_code_compact", Symbology::AztecCodeCompact),
("mands", Symbology::MarksAndSpencer),
("marks_and_spencer", Symbology::MarksAndSpencer),
("aztecrune", Symbology::AztecRune),
("aztec_rune", Symbology::AztecRune),
("channelcode", Symbology::ChannelCode),
("channel_code", Symbology::ChannelCode),
("gs1dldatamatrix", Symbology::Gs1DlDataMatrix),
("gs1-dl-datamatrix", Symbology::Gs1DlDataMatrix),
("gs1dlqrcode", Symbology::Gs1DlQrCode),
("gs1-dl-qrcode", Symbology::Gs1DlQrCode),
(
"datamatrixrectangularextension",
Symbology::DataMatrixRectangularExtension,
),
("dmre", Symbology::DataMatrixRectangularExtension),
(
"databartruncatedcomposite",
Symbology::CompositeDatabarTruncatedCca,
),
(
"databartruncatedcomposite_cca",
Symbology::CompositeDatabarTruncatedCca,
),
(
"databartruncatedcomposite_ccb",
Symbology::CompositeDatabarTruncatedCcb,
),
(
"databarstackedcomposite",
Symbology::CompositeDatabarStackedCca,
),
(
"databarstackedcomposite_cca",
Symbology::CompositeDatabarStackedCca,
),
(
"databarstackedcomposite_ccb",
Symbology::CompositeDatabarStackedCcb,
),
(
"databarstackedomnicomposite",
Symbology::CompositeDatabarStackedOmniCca,
),
(
"databarstackedomnicomposite_cca",
Symbology::CompositeDatabarStackedOmniCca,
),
(
"databarstackedomnicomposite_ccb",
Symbology::CompositeDatabarStackedOmniCcb,
),
(
"databarexpandedstackedcomposite",
Symbology::CompositeDatabarExpandedStackedCca,
),
(
"databarexpandedstackedcomposite_cca",
Symbology::CompositeDatabarExpandedStackedCca,
),
(
"databarexpandedstackedcomposite_ccb",
Symbology::CompositeDatabarExpandedStackedCcb,
),
];
for &(alias, target) in cases {
assert_eq!(
Symbology::from_id(alias),
Some(target),
"alias {alias:?} should route to {target:?}"
);
}
}
#[test]
fn every_symbology_has_a_display_name_and_category() {
for &s in Symbology::all() {
let name = s.display_name();
let cat = s.category();
assert!(!name.is_empty(), "{} has empty display_name", s.id());
assert!(!cat.is_empty(), "{} has empty category", s.id());
assert_ne!(name, s.id(), "{}: display_name == id", s.id());
}
}
#[test]
fn every_symbology_has_non_empty_default_data() {
for &s in Symbology::all() {
let d = s.default_data();
assert!(!d.is_empty(), "{} has empty default_data", s.id());
}
}
#[test]
fn all_contains_every_variant() {
let count = Symbology::all().len();
assert_eq!(
count, 154,
"Symbology::all() count drifted to {count} — did a new variant get added without listing it in all()?"
);
}
#[test]
fn ids_are_unique() {
let mut seen = std::collections::HashMap::<&'static str, Symbology>::new();
for &s in Symbology::all() {
if let Some(prev) = seen.insert(s.id(), s) {
panic!("duplicate id {:?}: {prev:?} and {s:?}", s.id());
}
}
}
#[test]
fn invalid_input_returns_error() {
let opts = Options::default();
let cases: &[(Symbology, &str, &str, &str)] = &[
(
Symbology::Ean13,
"abcdefghijkl",
"EAN-13",
"expected 12 or 13 digits",
),
(Symbology::Postnet, "abc", "PostNet", "digits only"),
(
Symbology::Code39,
"héllo",
"Code 39",
"does not support character",
),
(
Symbology::Codabar,
"1234",
"Codabar",
"must begin and end with",
),
(
Symbology::Pharmacode,
"0",
"Pharmacode",
"must be in [3, 131070]",
),
(
Symbology::Code128,
"",
"Code 128",
"payload must not be empty",
),
];
for &(sym, bad, symbology_anchor, predicate_anchor) in cases {
let err = render_svg(sym, bad, &opts)
.err()
.unwrap_or_else(|| panic!("{} should reject input {bad:?}, got Ok", sym.id()));
let msg = err.to_string();
assert!(
msg.contains(symbology_anchor),
"{} reject for {bad:?} must contain symbology anchor {symbology_anchor:?}, got {msg:?}",
sym.id(),
);
assert!(
msg.contains(predicate_anchor),
"{} reject for {bad:?} must contain predicate anchor {predicate_anchor:?} \
(kills mutations re-routing this case to a different rejection arm), \
got {msg:?}",
sym.id(),
);
}
}
#[test]
fn dotcode_renders_as_circles() {
let opts = Options::default();
for payload in ["Hello", "1234", "ABC123abc"] {
let svg = render_svg(Symbology::DotCode, payload, &opts)
.unwrap_or_else(|e| panic!("DotCode {payload:?} svg failed: {e}"));
assert!(svg.starts_with("<svg"));
assert!(
svg.contains("<circle "),
"DotCode SVG should contain <circle>"
);
assert!(
!svg.contains("<rect x="),
"DotCode SVG should NOT contain dot-positioned <rect> (only the background)",
);
let png =
render_png(Symbology::DotCode, payload, &opts).expect("DotCode PNG should succeed");
assert_eq!(&png[..4], &[0x89, b'P', b'N', b'G']);
assert!(png.len() > 100);
}
}
#[test]
fn dotcode_is_in_public_catalog() {
assert!(Symbology::all().contains(&Symbology::DotCode));
assert_eq!(Symbology::from_id("dotcode"), Some(Symbology::DotCode));
assert_eq!(Symbology::from_id("dot_code"), Some(Symbology::DotCode));
assert_eq!(Symbology::DotCode.id(), "dotcode");
assert_eq!(Symbology::DotCode.display_name(), "DotCode");
assert_eq!(Symbology::DotCode.category(), "2D - Matrix");
}
#[test]
fn substrate_rows_match_bwip_js_dimensions() {
use bwipp::{Encoded, Symbology};
let cases: &[(Symbology, &str, usize, usize)] = &[
#[cfg(not(feature = "prefer-native-qrcode"))]
(Symbology::QrCode, "https://example.com", 25, 25),
#[cfg(not(feature = "prefer-native-qrcode"))]
(Symbology::MicroQrCode, "12345", 11, 11),
(Symbology::Gs1QrCode, "(01)04012345123456(17)260101", 21, 21),
(Symbology::HibcQrCode, "A99912345/52001510X3", 25, 25),
(Symbology::DataMatrix, "hello", 12, 12),
(Symbology::DataMatrixRectangular, "hello", 18, 8),
(Symbology::Gs1DataMatrix, "(01)04012345123456", 16, 16),
(
Symbology::Gs1DataMatrixRectangular,
"(01)04012345123456",
32,
8,
),
(Symbology::HibcDataMatrix, "A99912345/52001510X3", 18, 18),
(
Symbology::HibcDataMatrixRectangular,
"A99912345/52001510X3",
26,
12,
),
(
Symbology::Gs1DlDataMatrix,
"https://id.gs1.org/01/04012345123456",
22,
22,
),
];
for &(sym, data, want_w, want_h) in cases {
let opts = bwipp::Options::default();
match sym.encode(data, &opts) {
Ok(Encoded::Matrix(m)) => {
assert_eq!(
(m.width(), m.height()),
(want_w, want_h),
"{}: size drift from bwip-js for {data:?} (got {}×{}, expected {want_w}×{want_h})",
sym.id(),
m.width(),
m.height(),
);
}
Ok(other) => panic!("{}: expected Matrix, got {:?}", sym.id(), other),
Err(e) => panic!("{} encode({data:?}) failed: {e}", sym.id()),
}
}
}
#[test]
fn categories_cover_known_groups() {
let cats: std::collections::HashSet<&'static str> =
Symbology::all().iter().map(|s| s.category()).collect();
for expected in [
"1D - Standard",
"1D - Retail / EAN / UPC",
"Postal",
"2D - Matrix",
"HIBC (Healthcare)",
] {
assert!(cats.contains(expected), "missing category: {expected}");
}
}
#[cfg(feature = "prefer-native-qrcode")]
#[test]
fn prefer_native_qrcode_routes_full_and_micro() {
use bwipp::{Encoded, Options, Symbology};
let opts = Options::default();
let full = Symbology::QrCode
.encode("HELLO WORLD", &opts)
.expect("native QR `HELLO WORLD` encode must succeed (V1 alphanumeric)");
match full {
Encoded::Matrix(m) => {
assert_eq!(m.width(), 21, "V1 expected for 11-char alphanumeric");
assert_eq!(m.height(), 21);
}
_ => panic!("expected Matrix encoding for QrCode"),
}
let micro = Symbology::MicroQrCode
.encode("1234", &opts)
.expect("native Micro QR `1234` encode must succeed (M1/M2 numeric)");
match micro {
Encoded::Matrix(m) => {
assert!(
m.width() >= 11 && m.width() <= 17,
"Micro QR width 11..=17, got {}",
m.width()
);
}
_ => panic!("expected Matrix encoding for MicroQrCode"),
}
}