okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
da73947
main
1//! The binary end to end: a file in, PNG pages and a text report out, or an 2//! image in and the same image fitted to an edge out. 3 4use std:: path::{ Path , PathBuf }; 5use std:: process:: Command ; 6 7struct Scratch ( PathBuf ); 8 9impl Scratch { 10fn new ( name : & str ) ->Scratch { 11let dir = std:: env:: temp_dir (). join ( format! ( "albedo-render-{name}-{}" , std:: process:: id ())); 12let _ = std:: fs:: remove_dir_all ( & dir); 13 std:: fs:: create_dir_all ( & dir). unwrap (); 14Scratch ( dir) 15} 16 17fn file ( & self , name : & str , content : & [ u8 ]) ->PathBuf { 18let path =self . 0 . join ( name); 19 std:: fs:: write ( & path, content). unwrap (); 20 path 21} 22} 23 24impl Drop for Scratch { 25fn drop ( & mut self ) { 26let _ = std:: fs:: remove_dir_all ( & self . 0 ); 27} 28} 29 30fn render ( file : & Path , out : & Path , args : & [ & str ]) ->( bool , String , String ) { 31let output =Command :: new ( env! ( "CARGO_BIN_EXE_albedo-render" )) 32. arg ( file) 33. arg ( "--out" ) 34. arg ( out) 35. args ( args) 36. output () 37. unwrap (); 38let text = |bytes : Vec < u8 > |String :: from_utf8 ( bytes). unwrap (); 39( 40 output. status . success (), 41text ( output. stdout ), 42text ( output. stderr ), 43) 44} 45 46fn png_size ( path : & Path ) ->( u32 , u32 ) { 47size ( & std:: fs:: read ( path). unwrap ()) 48} 49 50fn size ( bytes : & [ u8 ]) ->( u32 , u32 ) { 51assert_eq! ( & bytes[ ..8 ], b"\x89PNG\r\n\x1a\n" ); 52let field = |at : usize | u32:: from_be_bytes ( bytes[ at..at +4 ]. try_into (). unwrap ()); 53( field ( 16 ), field ( 20 )) 54} 55 56# [ test ] 57fn a_range_becomes_pages_and_a_report () { 58let scratch =Scratch :: new ( "pages" ); 59let source: String =( 1 ..=300 ) 60. map ( |n|format! ( "let value_{n} = {n};\n" )) 61. collect (); 62let file = scratch. file ( "sample.rs" , source. as_bytes ()); 63let ( ok, report, _) =render ( 64& file, 65& scratch. 0 , 66& [ 67"--start" , 68"11" , 69"--end" , 70"400" , 71"--max-rows" , 72"50" , 73"--max-images" , 74"2" , 75], 76); 77assert! ( ok); 78let lines: Vec < & str > = report. lines (). collect (); 79assert_eq! ( lines[ 0 ], "language rust" ); 80let image_lines: Vec < & str > = lines 81. iter () 82. copied () 83. filter ( |line| line. starts_with ( "image " )) 84. collect (); 85assert! ( !image_lines. is_empty ()); 86assert! ( image_lines. len () <=2 , "exceeded the requested image cap" ); 87let image_count = image_lines. len (); 88let mut next_row =11 ; 89for linein image_lines{ 90// Paths may contain spaces; the final three fields are the row range and dimensions. 91let mut fields = line. rsplitn ( 4 , ' ' ); 92let dimensions = fields. next (). unwrap (); 93let last: usize = fields. next (). unwrap (). parse (). unwrap (); 94let first: usize = fields. next (). unwrap (). parse (). unwrap (); 95let path = fields. next (). unwrap (). strip_prefix ( "image " ). unwrap (); 96assert_eq! ( first, next_row, "pages lost or repeated rows" ); 97assert! ( last >= first && last <=300 ); 98assert! ( last - first +1 <=50 , "exceeded the requested row cap" ); 99let ( width, height) =png_size ( Path :: new ( path)); 100assert! ( width >0 && height >0 ); 101assert_eq! ( dimensions, format! ( "{width}x{height}" )); 102 next_row = last +1 ; 103} 104assert_eq! ( lines. last (). unwrap (), & format! ( "remaining {next_row} 300" )); 105assert_eq! ( lines. len (), image_count +2 ); 106} 107 108# [ test ] 109fn keywords_are_colored_and_unknown_files_render_plain () { 110let scratch =Scratch :: new ( "colors" ); 111let rust = scratch. file ( "a.rs" , b"fn main() {}\n" ); 112let text = scratch. file ( "a.unknown" , b"fn main() {}\n" ); 113let ( _, report, _) =render ( & rust, & scratch. 0 , & [ "--start" , "1" , "--end" , "1" ]); 114assert_eq! ( report. lines (). next (), Some ( "language rust" )); 115let colored = std:: fs:: read ( scratch. 0 . join ( "view-1.png" )). unwrap (); 116let ( _, report, _) =render ( & text, & scratch. 0 , & [ "--start" , "1" , "--end" , "1" ]); 117assert_eq! ( 118 report. lines (). next (), 119Some ( "language plain unknown file type" ) 120); 121let plain = std:: fs:: read ( scratch. 0 . join ( "view-1.png" )). unwrap (); 122assert_ne! ( colored, plain); 123let ( _, report, _) =render ( 124& text, 125& scratch. 0 , 126& [ "--start" , "1" , "--end" , "1" , "--language" , "cobol" ], 127); 128assert_eq! ( 129 report. lines (). next (), 130Some ( "language plain no grammar for cobol" ) 131); 132} 133 134# [ test ] 135fn bad_requests_fail_with_a_reason () { 136let scratch =Scratch :: new ( "errors" ); 137let file = scratch. file ( "a.py" , b"x = 1\n" ); 138let binary = scratch. file ( "a.bin" , b"\x00\x01\x02" ); 139for ( target, args, reason) in [ 140( 141& file, 142vec! [ "--start" , "5" , "--end" , "6" ], 143"has 1 lines; --start 5 is past the end" , 144), 145( 146& file, 147vec! [ "--start" , "3" , "--end" , "2" ], 148"--end 2 is before --start 3" , 149), 150( 151& file, 152vec! [ "--start" , "1" ], 153"--start and --end are required" , 154), 155( 156& file, 157vec! [ "--start" , "1" , "--end" , "1" , "--columns" , "5" ], 158"--columns must be an integer from 20 to 400" , 159), 160( & binary, vec! [ "--start" , "1" , "--end" , "1" ], "looks binary" ), 161] { 162let ( ok, report, error) =render ( target, & scratch. 0 , & args); 163assert! ( !ok, "{args:?} succeeded" ); 164assert_eq! ( report, "" ); 165assert! ( error. contains ( reason), "{error:?} lacks {reason:?}" ); 166} 167} 168 169# [ test ] 170fn without_line_numbers_the_gutter_keeps_only_wrap_marks () { 171let scratch =Scratch :: new ( "gutter" ); 172let file = scratch. file ( "a.diff" , b"@@ -1 +1 @@\n-old\n+new\n" ); 173let page = scratch. 0 . join ( "view-1.png" ); 174render ( & file, & scratch. 0 , & [ "--start" , "1" , "--end" , "3" ]); 175let numbered = std:: fs:: read ( & page). unwrap (); 176let ( ok, report, _) =render ( 177& file, 178& scratch. 0 , 179& [ "--start" , "1" , "--end" , "3" , "--no-line-numbers" ], 180); 181assert! ( ok); 182assert_eq! ( report. lines (). next (), Some ( "language diff" )); 183let bare = std:: fs:: read ( & page). unwrap (); 184// One digit or one wrap mark: the same width, without the numbers. 185assert_eq! ( size ( & bare), size ( & numbered)); 186assert_ne! ( bare, numbered); 187} 188 189# [ test ] 190fn a_page_fits_inside_an_edge_and_keeps_its_aspect () { 191let scratch =Scratch :: new ( "fit" ); 192let source: String =( 1 ..=60 ). map ( |n|format! ( "line {n}\n" )). collect (); 193let file = scratch. file ( "sample.txt" , source. as_bytes ()); 194let pages = scratch. 0 . join ( "pages" ); 195 std:: fs:: create_dir_all ( & pages). unwrap (); 196assert! ( render ( & file, & pages, & [ "--start" , "1" , "--end" , "60" ]). 0 ); 197let page = pages. join ( "view-1.png" ); 198let ( width, height) =png_size ( & page); 199let ( ok, report, _) =render ( & page, & scratch. 0 , & [ "--fit" , "400" ]); 200assert! ( ok); 201let fitted = scratch. 0 . join ( "fit.png" ); 202let ( w, h) =png_size ( & fitted); 203assert_eq! ( report, format! ( "image {} {w}x{h}\n" , fitted. display ())); 204assert_eq! ( w. max ( h), 400 ); 205let expected = widthas f64 / heightas f64 ; 206assert! (( was f64 / has f64 - expected). abs () <0.02 ); 207let ( ok, _, error) =render ( & file, & scratch. 0 , & [ "--fit" , "400" ]); 208assert! ( !ok && error. contains ( "PNG, JPEG, or WebP" )); 209}