@@ -123,7 +123,7 @@ fn load_teams(project_root: &Path, team_file_globs: &[String]) -> std::result::R
123123 match Team :: from_team_file_path ( path. clone ( ) ) {
124124 Ok ( team) => teams. push ( team) ,
125125 Err ( e) => {
126- eprintln ! ( "Error parsing team file: {}" , e) ;
126+ eprintln ! ( "Error parsing team file: {}, path: {} " , e, path . display ( ) ) ;
127127 continue ;
128128 }
129129 }
@@ -311,3 +311,152 @@ fn source_priority(source: &Source) -> u8 {
311311 Source :: TeamYml => 5 ,
312312 }
313313}
314+
315+ #[ cfg( test) ]
316+ mod tests {
317+ use super :: * ;
318+ use crate :: project:: Team ;
319+ use std:: collections:: HashMap ;
320+ use tempfile:: tempdir;
321+
322+ fn build_config_for_temp ( frontend_glob : & str , ruby_glob : & str , vendored_path : & str ) -> crate :: config:: Config {
323+ crate :: config:: Config {
324+ owned_globs : vec ! [ "**/*" . to_string( ) ] ,
325+ ruby_package_paths : vec ! [ ruby_glob. to_string( ) ] ,
326+ javascript_package_paths : vec ! [ frontend_glob. to_string( ) ] ,
327+ team_file_glob : vec ! [ "config/teams/**/*.yml" . to_string( ) ] ,
328+ unowned_globs : vec ! [ ] ,
329+ vendored_gems_path : vendored_path. to_string ( ) ,
330+ cache_directory : "tmp/cache/codeowners" . to_string ( ) ,
331+ }
332+ }
333+
334+ fn team_named ( name : & str ) -> Team {
335+ Team {
336+ path : Path :: new ( "config/teams/foo.yml" ) . to_path_buf ( ) ,
337+ name : name. to_string ( ) ,
338+ github_team : format ! ( "@{}Team" , name) ,
339+ owned_globs : vec ! [ ] ,
340+ subtracted_globs : vec ! [ ] ,
341+ owned_gems : vec ! [ ] ,
342+ avoid_ownership : false ,
343+ }
344+ }
345+
346+ #[ test]
347+ fn test_read_top_of_file_team_parses_at_and_colon_forms ( ) {
348+ let td = tempdir ( ) . unwrap ( ) ;
349+
350+ // @team form
351+ let file_at = td. path ( ) . join ( "at_form.rb" ) ;
352+ std:: fs:: write ( & file_at, "# @team Payroll\n puts 'x'\n " ) . unwrap ( ) ;
353+ assert_eq ! ( read_top_of_file_team( & file_at) , Some ( "Payroll" . to_string( ) ) ) ;
354+
355+ // team: form (case-insensitive, allows spaces)
356+ let file_colon = td. path ( ) . join ( "colon_form.rb" ) ;
357+ std:: fs:: write ( & file_colon, "# Team: Payments\n puts 'y'\n " ) . unwrap ( ) ;
358+ assert_eq ! ( read_top_of_file_team( & file_colon) , Some ( "Payments" . to_string( ) ) ) ;
359+ }
360+
361+ #[ test]
362+ fn test_most_specific_directory_owner_prefers_deeper ( ) {
363+ let td = tempdir ( ) . unwrap ( ) ;
364+ let project_root = td. path ( ) ;
365+
366+ // Build directories
367+ let deep_dir = project_root. join ( "a/b/c" ) ;
368+ std:: fs:: create_dir_all ( & deep_dir) . unwrap ( ) ;
369+ let mid_dir = project_root. join ( "a/b" ) ;
370+ let top_dir = project_root. join ( "a" ) ;
371+
372+ // Write .codeowner files
373+ std:: fs:: write ( top_dir. join ( ".codeowner" ) , "TopTeam" ) . unwrap ( ) ;
374+ std:: fs:: write ( mid_dir. join ( ".codeowner" ) , "MidTeam" ) . unwrap ( ) ;
375+ std:: fs:: write ( deep_dir. join ( ".codeowner" ) , "DeepTeam" ) . unwrap ( ) ;
376+
377+ // Build teams_by_name
378+ let mut tbn: HashMap < String , Team > = HashMap :: new ( ) ;
379+ for name in [ "TopTeam" , "MidTeam" , "DeepTeam" ] {
380+ let t = team_named ( name) ;
381+ tbn. insert ( t. name . clone ( ) , t) ;
382+ }
383+
384+ let rel_file = Path :: new ( "a/b/c/file.rb" ) ;
385+ let result = most_specific_directory_owner ( project_root, rel_file, & tbn) . unwrap ( ) ;
386+ match result. 1 {
387+ Source :: Directory ( path) => {
388+ assert ! ( path. ends_with( "a/b/c" ) , "expected deepest directory, got {}" , path) ;
389+ }
390+ _ => panic ! ( "expected Directory source" ) ,
391+ }
392+ assert_eq ! ( result. 0 , "DeepTeam" ) ;
393+ }
394+
395+ #[ test]
396+ fn test_nearest_package_owner_ruby_and_js ( ) {
397+ let td = tempdir ( ) . unwrap ( ) ;
398+ let project_root = td. path ( ) ;
399+ let config = build_config_for_temp ( "frontend/**/*" , "packs/**/*" , "vendored" ) ;
400+
401+ // Ruby package
402+ let ruby_pkg = project_root. join ( "packs/payroll" ) ;
403+ std:: fs:: create_dir_all ( & ruby_pkg) . unwrap ( ) ;
404+ std:: fs:: write (
405+ ruby_pkg. join ( "package.yml" ) ,
406+ "---\n owner: Payroll\n " ,
407+ )
408+ . unwrap ( ) ;
409+
410+ // JS package
411+ let js_pkg = project_root. join ( "frontend/flow" ) ;
412+ std:: fs:: create_dir_all ( & js_pkg) . unwrap ( ) ;
413+ std:: fs:: write (
414+ js_pkg. join ( "package.json" ) ,
415+ r#"{"metadata": {"owner": "UX"}}"# ,
416+ )
417+ . unwrap ( ) ;
418+
419+ // Teams map
420+ let mut tbn: HashMap < String , Team > = HashMap :: new ( ) ;
421+ for name in [ "Payroll" , "UX" ] {
422+ let t = team_named ( name) ;
423+ tbn. insert ( t. name . clone ( ) , t) ;
424+ }
425+
426+ // Ruby nearest
427+ let rel_ruby = Path :: new ( "packs/payroll/app/models/thing.rb" ) ;
428+ let ruby_owner = nearest_package_owner ( project_root, rel_ruby, & config, & tbn) . unwrap ( ) ;
429+ assert_eq ! ( ruby_owner. 0 , "Payroll" ) ;
430+ match ruby_owner. 1 {
431+ Source :: Package ( pkg_path, glob) => {
432+ assert ! ( pkg_path. ends_with( "packs/payroll/package.yml" ) ) ;
433+ assert_eq ! ( glob, "packs/payroll/**/**" ) ;
434+ }
435+ _ => panic ! ( "expected Package source for ruby" ) ,
436+ }
437+
438+ // JS nearest
439+ let rel_js = Path :: new ( "frontend/flow/src/index.ts" ) ;
440+ let js_owner = nearest_package_owner ( project_root, rel_js, & config, & tbn) . unwrap ( ) ;
441+ assert_eq ! ( js_owner. 0 , "UX" ) ;
442+ match js_owner. 1 {
443+ Source :: Package ( pkg_path, glob) => {
444+ assert ! ( pkg_path. ends_with( "frontend/flow/package.json" ) ) ;
445+ assert_eq ! ( glob, "frontend/flow/**/**" ) ;
446+ }
447+ _ => panic ! ( "expected Package source for js" ) ,
448+ }
449+ }
450+
451+ #[ test]
452+ fn test_vendored_gem_owner ( ) {
453+ let config = build_config_for_temp ( "frontend/**/*" , "packs/**/*" , "vendored" ) ;
454+ let mut teams: Vec < Team > = vec ! [ team_named( "Payroll" ) ] ;
455+ teams[ 0 ] . owned_gems = vec ! [ "awesome_gem" . to_string( ) ] ;
456+
457+ let path = Path :: new ( "vendored/awesome_gem/lib/a.rb" ) ;
458+ let result = vendored_gem_owner ( path, & config, & teams) . unwrap ( ) ;
459+ assert_eq ! ( result. 0 , "Payroll" ) ;
460+ matches ! ( result. 1 , Source :: TeamGem ) ;
461+ }
462+ }
0 commit comments