···374 Printf.sprintf "Element %s is missing one or more of the following attributes: [%s]."
375 (q element) attrs_str
376 | `Attr (`Bad_value (`Elem element, `Attr attr, `Value value, `Reason reason)) ->
377- Printf.sprintf "Bad value %s for attribute %s on element %s: %s"
378- (q value) (q attr) (q element) reason
0000379 | `Attr (`Bad_value_generic (`Message message)) -> message
380 | `Attr (`Duplicate_id (`Id id)) ->
381 Printf.sprintf "Duplicate ID %s." (q id)
···374 Printf.sprintf "Element %s is missing one or more of the following attributes: [%s]."
375 (q element) attrs_str
376 | `Attr (`Bad_value (`Elem element, `Attr attr, `Value value, `Reason reason)) ->
377+ if reason = "" then
378+ Printf.sprintf "Bad value %s for attribute %s on element %s."
379+ (q value) (q attr) (q element)
380+ else
381+ Printf.sprintf "Bad value %s for attribute %s on element %s: %s"
382+ (q value) (q attr) (q element) reason
383 | `Attr (`Bad_value_generic (`Message message)) -> message
384 | `Attr (`Duplicate_id (`Id id)) ->
385 Printf.sprintf "Duplicate ID %s." (q id)
+249-37
test/test_validator.ml
···402 } in
403 Report.generate_report report output_path
404405-let () =
406- (* Parse command line arguments *)
407- let args = Array.to_list Sys.argv |> List.tl in
408- let is_strict = List.mem "--strict" args in
409- let non_flag_args = List.filter (fun s -> not (String.length s > 0 && s.[0] = '-')) args in
410- let tests_dir = match non_flag_args with x :: _ -> x | [] -> "validator/tests" in
411- let report_path = match non_flag_args with _ :: x :: _ -> x | _ -> "test_validator_report.html" in
412-413- (* Apply strict mode if requested - use exact_message which requires exact text but not typed codes *)
414- if is_strict then begin
415- strictness := Expected_message.exact_message;
416- Printf.printf "Running in STRICT mode (exact message matching required)\n%!"
417- end;
418-419- Printf.printf "Loading messages.json...\n%!";
420- let messages_path = Filename.concat tests_dir "messages.json" in
421- let messages = Validator_messages.load messages_path in
422- Printf.printf "Loaded %d expected messages\n%!" (Validator_messages.count messages);
423-424- Printf.printf "Discovering test files...\n%!";
425- let tests = discover_tests tests_dir in
426- Printf.printf "Found %d test files\n%!" (List.length tests);
427-428- Printf.printf "Running tests...\n%!";
429 let total = List.length tests in
430 let results = List.mapi (fun i test ->
431 Printf.printf "\r[%d/%d] %s%!" (i + 1) total test.relative_path;
432 run_test messages test
433 ) tests in
434 Printf.printf "\n%!";
0435436- (* Print failing isvalid tests *)
000437 let failing_isvalid = List.filter (fun r ->
438 r.file.expected = Valid && not r.passed
439 ) results in
440 if failing_isvalid <> [] then begin
441- Printf.printf "\n=== Failing isvalid tests ===\n";
442 List.iter (fun r ->
443- Printf.printf "%s: %s\n" r.file.relative_path r.details
444 ) failing_isvalid
445 end;
446447- (* Print failing haswarn tests *)
448 let failing_haswarn = List.filter (fun r ->
449 r.file.expected = HasWarning && not r.passed
450 ) results in
451 if failing_haswarn <> [] then begin
452- Printf.printf "\n=== Failing haswarn tests ===\n";
453 List.iter (fun r ->
454- Printf.printf "%s\n" r.file.relative_path
455 ) failing_haswarn
456 end;
457458- (* Print failing novalid tests *)
459 let failing_novalid = List.filter (fun r ->
460 r.file.expected = Invalid && not r.passed
461 ) results in
462 if failing_novalid <> [] then begin
463- Printf.printf "\n=== Failing novalid tests (first 50) ===\n";
464 List.iteri (fun i r ->
465- if i < 50 then Printf.printf "%s\n" r.file.relative_path
466 ) failing_novalid
467 end;
468469- print_summary results;
470- generate_html_report results report_path;
000000000000000000000000000000000000000000000000000000000000000000471472- let failed_count = List.filter (fun r -> not r.passed) results |> List.length in
473- exit (if failed_count > 0 then 1 else 0)
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···402 } in
403 Report.generate_report report output_path
404405+(** Run tests with a given strictness and return results *)
406+let run_all_tests ~mode_name ~strictness_setting messages tests =
407+ strictness := strictness_setting;
408+ Printf.printf "\n=== Running in %s mode ===\n%!" mode_name;
00000000000000000000409 let total = List.length tests in
410 let results = List.mapi (fun i test ->
411 Printf.printf "\r[%d/%d] %s%!" (i + 1) total test.relative_path;
412 run_test messages test
413 ) tests in
414 Printf.printf "\n%!";
415+ results
416417+(** Print failures for a test run *)
418+let print_failures mode_name results =
419+ Printf.printf "\n--- %s mode results ---\n" mode_name;
420+421 let failing_isvalid = List.filter (fun r ->
422 r.file.expected = Valid && not r.passed
423 ) results in
424 if failing_isvalid <> [] then begin
425+ Printf.printf "Failing isvalid tests:\n";
426 List.iter (fun r ->
427+ Printf.printf " %s: %s\n" r.file.relative_path r.details
428 ) failing_isvalid
429 end;
4300431 let failing_haswarn = List.filter (fun r ->
432 r.file.expected = HasWarning && not r.passed
433 ) results in
434 if failing_haswarn <> [] then begin
435+ Printf.printf "Failing haswarn tests:\n";
436 List.iter (fun r ->
437+ Printf.printf " %s\n" r.file.relative_path
438 ) failing_haswarn
439 end;
4400441 let failing_novalid = List.filter (fun r ->
442 r.file.expected = Invalid && not r.passed
443 ) results in
444 if failing_novalid <> [] then begin
445+ Printf.printf "Failing novalid tests (first 20):\n";
446 List.iteri (fun i r ->
447+ if i < 20 then Printf.printf " %s\n" r.file.relative_path
448 ) failing_novalid
449 end;
450451+ let passed = List.filter (fun r -> r.passed) results |> List.length in
452+ let total = List.length results in
453+ Printf.printf "%s: %d/%d passed (%.1f%%)\n%!" mode_name passed total
454+ (100.0 *. float_of_int passed /. float_of_int total)
455+456+(** Generate combined HTML report for both modes *)
457+let generate_combined_html_report ~lenient_results ~strict_results output_path =
458+ (* Helper to build file results from a set of results *)
459+ let build_file_results results =
460+ let by_category = group_by_category results in
461+ List.map (fun (category, tests) ->
462+ let passed_count = List.filter (fun r -> r.passed) tests |> List.length in
463+ let failed_count = List.length tests - passed_count in
464+ let test_results = List.mapi (fun i r ->
465+ let outcome_str = match r.file.expected with
466+ | Valid -> "isvalid"
467+ | Invalid -> "novalid"
468+ | HasWarning -> "haswarn"
469+ | Unknown -> "unknown"
470+ in
471+ let description = Printf.sprintf "[%s] %s" outcome_str (Filename.basename r.file.relative_path) in
472+ let expected = match r.expected_message with
473+ | Some m -> m
474+ | None -> match r.file.expected with
475+ | Valid -> "(should produce no errors or warnings)"
476+ | Invalid -> "(should produce at least one error)"
477+ | HasWarning -> "(should produce at least one warning)"
478+ | Unknown -> "(unknown test type)"
479+ in
480+ let actual_str =
481+ let errors = if r.actual_errors = [] then ""
482+ else "Errors:\n • " ^ String.concat "\n • " r.actual_errors in
483+ let warnings = if r.actual_warnings = [] then ""
484+ else "Warnings:\n • " ^ String.concat "\n • " r.actual_warnings in
485+ let infos = if r.actual_infos = [] then ""
486+ else "Info:\n • " ^ String.concat "\n • " r.actual_infos in
487+ if errors = "" && warnings = "" && infos = "" then "(no messages produced)"
488+ else String.trim (errors ^ (if errors <> "" && warnings <> "" then "\n\n" else "") ^
489+ warnings ^ (if (errors <> "" || warnings <> "") && infos <> "" then "\n\n" else "") ^
490+ infos)
491+ in
492+ let match_quality_str = match r.match_quality with
493+ | Some q -> Expected_message.match_quality_to_string q
494+ | None -> "N/A"
495+ in
496+ Report.{
497+ test_num = i + 1;
498+ description;
499+ input = r.file.relative_path;
500+ expected;
501+ actual = actual_str;
502+ success = r.passed;
503+ details = [
504+ ("Result", r.details);
505+ ("Match Quality", match_quality_str);
506+ ];
507+ raw_test_data = read_html_source r.file.path;
508+ }
509+ ) tests in
510+ Report.{
511+ filename = category;
512+ test_type = "HTML5 Validator";
513+ passed_count;
514+ failed_count;
515+ tests = test_results;
516+ }
517+ ) by_category
518+ in
519520+ let compute_stats results mode_name =
521+ let total_passed = List.filter (fun r -> r.passed) results |> List.length in
522+ let total_failed = List.length results - total_passed in
523+ let count_quality q = List.filter (fun r ->
524+ match r.match_quality with Some mq -> mq = q | None -> false
525+ ) results |> List.length in
526+ let match_quality_stats : Report.match_quality_stats = {
527+ exact_matches = count_quality Expected_message.Exact_match;
528+ code_matches = count_quality Expected_message.Code_match;
529+ message_matches = count_quality Expected_message.Message_match;
530+ substring_matches = count_quality Expected_message.Substring_match;
531+ severity_mismatches = count_quality Expected_message.Severity_mismatch;
532+ no_matches = count_quality Expected_message.No_match;
533+ not_applicable = List.filter (fun r -> r.match_quality = None) results |> List.length;
534+ } in
535+ let isvalid_results = List.filter (fun r -> r.file.expected = Valid) results in
536+ let novalid_results = List.filter (fun r -> r.file.expected = Invalid) results in
537+ let haswarn_results = List.filter (fun r -> r.file.expected = HasWarning) results in
538+ let count_passed rs = List.filter (fun r -> r.passed) rs |> List.length in
539+ let test_type_stats : Report.test_type_stats = {
540+ isvalid_passed = count_passed isvalid_results;
541+ isvalid_total = List.length isvalid_results;
542+ novalid_passed = count_passed novalid_results;
543+ novalid_total = List.length novalid_results;
544+ haswarn_passed = count_passed haswarn_results;
545+ haswarn_total = List.length haswarn_results;
546+ } in
547+ (total_passed, total_failed, match_quality_stats, test_type_stats, mode_name)
548+ in
549+550+ let lenient_stats = compute_stats lenient_results "lenient" in
551+ let strict_stats = compute_stats strict_results "strict" in
552+553+ (* Use strict results for the main report, but include both in description *)
554+ let (strict_passed, strict_failed, strict_mq, strict_tt, _) = strict_stats in
555+ let (lenient_passed, _lenient_failed, _, _, _) = lenient_stats in
556+557+ let now = Unix.gettimeofday () in
558+ let tm = Unix.localtime now in
559+ let timestamp = Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d"
560+ (tm.Unix.tm_year + 1900) (tm.Unix.tm_mon + 1) tm.Unix.tm_mday
561+ tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec in
562+563+ let total = List.length strict_results in
564+ let description = Printf.sprintf
565+ "Tests from the Nu HTML Validator (W3C's official HTML checker). \
566+ Tests validate HTML5 conformance including element nesting, required attributes, \
567+ ARIA roles, obsolete elements, and more.\n\n\
568+ LENIENT mode: %d/%d passed (%.1f%%) - allows substring matching\n\
569+ STRICT mode: %d/%d passed (%.1f%%) - requires exact message matching"
570+ lenient_passed total (100.0 *. float_of_int lenient_passed /. float_of_int total)
571+ strict_passed total (100.0 *. float_of_int strict_passed /. float_of_int total)
572+ in
573+574+ let report : Report.report = {
575+ title = "Nu HTML Validator Tests (Lenient + Strict)";
576+ test_type = "validator";
577+ description;
578+ files = build_file_results strict_results; (* Show strict results in detail *)
579+ total_passed = strict_passed;
580+ total_failed = strict_failed;
581+ match_quality = Some strict_mq;
582+ test_type_breakdown = Some strict_tt;
583+ strictness_mode = Some (Printf.sprintf "BOTH (Lenient: %d/%d, Strict: %d/%d)"
584+ lenient_passed total strict_passed total);
585+ run_timestamp = Some timestamp;
586+ } in
587+ Report.generate_report report output_path
588+589+let () =
590+ (* Parse command line arguments *)
591+ let args = Array.to_list Sys.argv |> List.tl in
592+ let is_strict = List.mem "--strict" args in
593+ let is_both = List.mem "--both" args in
594+ let non_flag_args = List.filter (fun s -> not (String.length s > 0 && s.[0] = '-')) args in
595+ let tests_dir = match non_flag_args with x :: _ -> x | [] -> "validator/tests" in
596+ let report_path = match non_flag_args with _ :: x :: _ -> x | _ -> "test_validator_report.html" in
597+598+ Printf.printf "Loading messages.json...\n%!";
599+ let messages_path = Filename.concat tests_dir "messages.json" in
600+ let messages = Validator_messages.load messages_path in
601+ Printf.printf "Loaded %d expected messages\n%!" (Validator_messages.count messages);
602+603+ Printf.printf "Discovering test files...\n%!";
604+ let tests = discover_tests tests_dir in
605+ Printf.printf "Found %d test files\n%!" (List.length tests);
606+607+ if is_both then begin
608+ (* Run both modes *)
609+ let lenient_results = run_all_tests ~mode_name:"LENIENT"
610+ ~strictness_setting:Expected_message.lenient messages tests in
611+ let strict_results = run_all_tests ~mode_name:"STRICT"
612+ ~strictness_setting:Expected_message.exact_message messages tests in
613+614+ print_failures "LENIENT" lenient_results;
615+ print_failures "STRICT" strict_results;
616+617+ Printf.printf "\n=== Summary ===\n";
618+ let lenient_passed = List.filter (fun r -> r.passed) lenient_results |> List.length in
619+ let strict_passed = List.filter (fun r -> r.passed) strict_results |> List.length in
620+ let total = List.length tests in
621+ Printf.printf "LENIENT: %d/%d (%.1f%%)\n" lenient_passed total
622+ (100.0 *. float_of_int lenient_passed /. float_of_int total);
623+ Printf.printf "STRICT: %d/%d (%.1f%%)\n" strict_passed total
624+ (100.0 *. float_of_int strict_passed /. float_of_int total);
625+626+ generate_combined_html_report ~lenient_results ~strict_results report_path;
627+ Printf.printf "\nHTML report written to: %s\n" report_path;
628+629+ (* Exit with error if strict mode has failures *)
630+ let strict_failed = List.filter (fun r -> not r.passed) strict_results |> List.length in
631+ exit (if strict_failed > 0 then 1 else 0)
632+ end else begin
633+ (* Single mode (original behavior) *)
634+ if is_strict then begin
635+ strictness := Expected_message.exact_message;
636+ Printf.printf "Running in STRICT mode (exact message matching required)\n%!"
637+ end;
638+639+ Printf.printf "Running tests...\n%!";
640+ let total = List.length tests in
641+ let results = List.mapi (fun i test ->
642+ Printf.printf "\r[%d/%d] %s%!" (i + 1) total test.relative_path;
643+ run_test messages test
644+ ) tests in
645+ Printf.printf "\n%!";
646+647+ (* Print failing isvalid tests *)
648+ let failing_isvalid = List.filter (fun r ->
649+ r.file.expected = Valid && not r.passed
650+ ) results in
651+ if failing_isvalid <> [] then begin
652+ Printf.printf "\n=== Failing isvalid tests ===\n";
653+ List.iter (fun r ->
654+ Printf.printf "%s: %s\n" r.file.relative_path r.details
655+ ) failing_isvalid
656+ end;
657+658+ (* Print failing haswarn tests *)
659+ let failing_haswarn = List.filter (fun r ->
660+ r.file.expected = HasWarning && not r.passed
661+ ) results in
662+ if failing_haswarn <> [] then begin
663+ Printf.printf "\n=== Failing haswarn tests ===\n";
664+ List.iter (fun r ->
665+ Printf.printf "%s\n" r.file.relative_path
666+ ) failing_haswarn
667+ end;
668+669+ (* Print failing novalid tests *)
670+ let failing_novalid = List.filter (fun r ->
671+ r.file.expected = Invalid && not r.passed
672+ ) results in
673+ if failing_novalid <> [] then begin
674+ Printf.printf "\n=== Failing novalid tests (first 50) ===\n";
675+ List.iteri (fun i r ->
676+ if i < 50 then Printf.printf "%s\n" r.file.relative_path
677+ ) failing_novalid
678+ end;
679+680+ print_summary results;
681+ generate_html_report results report_path;
682+683+ let failed_count = List.filter (fun r -> not r.passed) results |> List.length in
684+ exit (if failed_count > 0 then 1 else 0)
685+ end