3d7b09bcef
60 Design + 80 Schutz + 60 Information — alle mit Normenreferenzen. Subtypes: geometry, force_energy, material, ergonomics, control_design, fixed_guard, movable_guard, electro_sensitive, emergency_stop, electrical/thermal/fluid protection, extraction, signage, manual, training, ppe, organizational, marking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
174 lines
4.8 KiB
Go
174 lines
4.8 KiB
Go
package iace
|
|
|
|
import "testing"
|
|
|
|
// TestControlsLibrary_UniqueIDs verifies all control IDs are unique.
|
|
func TestControlsLibrary_UniqueIDs(t *testing.T) {
|
|
seen := make(map[string]bool)
|
|
for _, e := range GetControlsLibrary() {
|
|
if e.ID == "" {
|
|
t.Errorf("control has empty ID")
|
|
continue
|
|
}
|
|
if seen[e.ID] {
|
|
t.Errorf("duplicate control ID: %s", e.ID)
|
|
}
|
|
seen[e.ID] = true
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_HasExamples verifies measures have examples.
|
|
func TestProtectiveMeasures_HasExamples(t *testing.T) {
|
|
withExamples := 0
|
|
for _, e := range GetProtectiveMeasureLibrary() {
|
|
if len(e.Examples) > 0 {
|
|
withExamples++
|
|
}
|
|
}
|
|
total := len(GetProtectiveMeasureLibrary())
|
|
threshold := total * 80 / 100
|
|
if withExamples < threshold {
|
|
t.Errorf("only %d/%d measures have examples, want at least %d", withExamples, total, threshold)
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_ThreeReductionTypesPresent verifies all 3 types exist.
|
|
func TestProtectiveMeasures_ThreeReductionTypesPresent(t *testing.T) {
|
|
types := make(map[string]int)
|
|
for _, e := range GetProtectiveMeasureLibrary() {
|
|
types[e.ReductionType]++
|
|
}
|
|
// Accept both naming variants
|
|
designCount := types["design"]
|
|
protectiveCount := types["protective"] + types["protection"]
|
|
infoCount := types["information"]
|
|
|
|
if designCount == 0 {
|
|
t.Error("no measures with reduction type design")
|
|
}
|
|
if protectiveCount == 0 {
|
|
t.Error("no measures with reduction type protective/protection")
|
|
}
|
|
if infoCount == 0 {
|
|
t.Error("no measures with reduction type information")
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_TagFieldAccessible verifies the Tags field is accessible.
|
|
func TestProtectiveMeasures_TagFieldAccessible(t *testing.T) {
|
|
measures := GetProtectiveMeasureLibrary()
|
|
if len(measures) == 0 {
|
|
t.Fatal("no measures returned")
|
|
}
|
|
// Tags field exists but may not be populated yet
|
|
_ = measures[0].Tags
|
|
}
|
|
|
|
// TestProtectiveMeasures_HazardCategoryNotEmpty verifies HazardCategory is populated.
|
|
func TestProtectiveMeasures_HazardCategoryNotEmpty(t *testing.T) {
|
|
for _, e := range GetProtectiveMeasureLibrary() {
|
|
if e.HazardCategory == "" {
|
|
t.Errorf("measure %s (%s): HazardCategory is empty", e.ID, e.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_Count200 verifies exactly 200 measures exist.
|
|
func TestProtectiveMeasures_Count200(t *testing.T) {
|
|
entries := GetProtectiveMeasureLibrary()
|
|
if len(entries) != 200 {
|
|
t.Fatalf("got %d protective measures, want exactly 200", len(entries))
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_Count160 verifies at least 160 measures exist (legacy gate).
|
|
func TestProtectiveMeasures_Count160(t *testing.T) {
|
|
entries := GetProtectiveMeasureLibrary()
|
|
if len(entries) < 160 {
|
|
t.Fatalf("got %d protective measures, want at least 160", len(entries))
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_NormReferencesPopulated verifies most measures have norm refs.
|
|
func TestProtectiveMeasures_NormReferencesPopulated(t *testing.T) {
|
|
withRefs := 0
|
|
for _, e := range GetProtectiveMeasureLibrary() {
|
|
if len(e.NormReferences) > 0 {
|
|
withRefs++
|
|
}
|
|
}
|
|
total := len(GetProtectiveMeasureLibrary())
|
|
threshold := total * 90 / 100
|
|
if withRefs < threshold {
|
|
t.Errorf("only %d/%d measures have NormReferences, want at least %d", withRefs, total, threshold)
|
|
}
|
|
}
|
|
|
|
// TestProtectiveMeasures_DesignProtectionInfoDistribution verifies balanced distribution.
|
|
func TestProtectiveMeasures_DesignProtectionInfoDistribution(t *testing.T) {
|
|
types := make(map[string]int)
|
|
for _, e := range GetProtectiveMeasureLibrary() {
|
|
types[e.ReductionType]++
|
|
}
|
|
design := types["design"]
|
|
protection := types["protection"]
|
|
information := types["information"]
|
|
|
|
if design < 50 {
|
|
t.Errorf("design measures: %d, want >= 50", design)
|
|
}
|
|
if protection < 70 {
|
|
t.Errorf("protection measures: %d, want >= 70", protection)
|
|
}
|
|
if information < 50 {
|
|
t.Errorf("information measures: %d, want >= 50", information)
|
|
}
|
|
t.Logf("Distribution: design=%d, protection=%d, information=%d", design, protection, information)
|
|
}
|
|
|
|
// TestProtectiveMeasures_IDSequential verifies IDs run M001-M200 without gaps.
|
|
func TestProtectiveMeasures_IDSequential(t *testing.T) {
|
|
entries := GetProtectiveMeasureLibrary()
|
|
for i, e := range entries {
|
|
expected := "M" + padID(i+1)
|
|
if e.ID != expected {
|
|
t.Errorf("entries[%d]: got ID %q, want %q", i, e.ID, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func padID(n int) string {
|
|
if n < 10 {
|
|
return "00" + itoa(n)
|
|
}
|
|
if n < 100 {
|
|
return "0" + itoa(n)
|
|
}
|
|
return itoa(n)
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
s := ""
|
|
for n > 0 {
|
|
s = string(rune('0'+n%10)) + s
|
|
n /= 10
|
|
}
|
|
return s
|
|
}
|
|
|
|
// TestProtectiveMeasures_SubTypesPresent verifies subtypes are used.
|
|
func TestProtectiveMeasures_SubTypesPresent(t *testing.T) {
|
|
subtypes := make(map[string]int)
|
|
for _, e := range GetProtectiveMeasureLibrary() {
|
|
if e.SubType != "" {
|
|
subtypes[e.SubType]++
|
|
}
|
|
}
|
|
if len(subtypes) < 3 {
|
|
t.Errorf("expected at least 3 different subtypes, got %d: %v", len(subtypes), subtypes)
|
|
}
|
|
}
|