@@ -1105,3 +1105,208 @@ impl Provider for ApplicationsProvider {
11051105 Box :: pin ( async move { result } )
11061106 }
11071107}
1108+
1109+ #[ cfg( test) ]
1110+ mod tests {
1111+ use super :: * ;
1112+ use std:: fs;
1113+
1114+ /// A self-cleaning temporary directory (avoids pulling in a dev-dependency).
1115+ struct TempDir {
1116+ path : PathBuf ,
1117+ }
1118+
1119+ impl TempDir {
1120+ fn new ( ) -> Self {
1121+ let path = std:: env:: temp_dir ( ) . join ( format ! ( "datacube-test-{}" , uuid:: Uuid :: new_v4( ) ) ) ;
1122+ fs:: create_dir_all ( & path) . unwrap ( ) ;
1123+ Self { path }
1124+ }
1125+
1126+ fn write ( & self , name : & str , contents : & str ) -> PathBuf {
1127+ let p = self . path . join ( name) ;
1128+ fs:: write ( & p, contents) . unwrap ( ) ;
1129+ p
1130+ }
1131+ }
1132+
1133+ impl Drop for TempDir {
1134+ fn drop ( & mut self ) {
1135+ let _ = fs:: remove_dir_all ( & self . path ) ;
1136+ }
1137+ }
1138+
1139+ fn make_entry ( id : & str , name : & str ) -> AppEntry {
1140+ AppEntry {
1141+ id : id. to_string ( ) ,
1142+ path : PathBuf :: from ( format ! ( "/usr/share/applications/{id}.desktop" ) ) ,
1143+ name : name. to_string ( ) ,
1144+ generic_name : None ,
1145+ comment : None ,
1146+ icon : "app-icon" . to_string ( ) ,
1147+ icon_path : None ,
1148+ keywords : Vec :: new ( ) ,
1149+ terminal : false ,
1150+ launch_count : 0 ,
1151+ source : AppSource :: Native ,
1152+ }
1153+ }
1154+
1155+ /// Build a provider directly from a set of entries, bypassing the
1156+ /// filesystem scan and background loader.
1157+ fn provider_with ( entries : Vec < AppEntry > ) -> ApplicationsProvider {
1158+ let map: HashMap < String , AppEntry > =
1159+ entries. into_iter ( ) . map ( |e| ( e. id . clone ( ) , e) ) . collect ( ) ;
1160+ ApplicationsProvider {
1161+ apps : Arc :: new ( RwLock :: new ( map) ) ,
1162+ path_to_id : Arc :: new ( RwLock :: new ( HashMap :: new ( ) ) ) ,
1163+ matcher : SkimMatcherV2 :: default ( ) ,
1164+ extra_dirs : Vec :: new ( ) ,
1165+ watcher : None ,
1166+ }
1167+ }
1168+
1169+ #[ test]
1170+ fn app_source_from_path ( ) {
1171+ assert_eq ! (
1172+ AppSource :: from_path( Path :: new(
1173+ "/var/lib/flatpak/exports/share/applications/org.x.desktop"
1174+ ) ) ,
1175+ AppSource :: Flatpak
1176+ ) ;
1177+ assert_eq ! (
1178+ AppSource :: from_path( Path :: new( "/var/lib/snapd/desktop/applications/foo.desktop" ) ) ,
1179+ AppSource :: Snap
1180+ ) ;
1181+ assert_eq ! (
1182+ AppSource :: from_path( Path :: new( "/usr/share/applications/foo.desktop" ) ) ,
1183+ AppSource :: Native
1184+ ) ;
1185+ }
1186+
1187+ #[ test]
1188+ fn app_source_as_str ( ) {
1189+ assert_eq ! ( AppSource :: Native . as_str( ) , "native" ) ;
1190+ assert_eq ! ( AppSource :: Flatpak . as_str( ) , "flatpak" ) ;
1191+ assert_eq ! ( AppSource :: Snap . as_str( ) , "snap" ) ;
1192+ }
1193+
1194+ #[ test]
1195+ fn detects_desktop_files ( ) {
1196+ assert ! ( ApplicationsProvider :: is_desktop_file( Path :: new(
1197+ "/a/b/foo.desktop"
1198+ ) ) ) ;
1199+ assert ! ( !ApplicationsProvider :: is_desktop_file( Path :: new(
1200+ "/a/b/foo.txt"
1201+ ) ) ) ;
1202+ assert ! ( !ApplicationsProvider :: is_desktop_file( Path :: new(
1203+ "/a/b/foo"
1204+ ) ) ) ;
1205+ }
1206+
1207+ #[ test]
1208+ fn parse_desktop_file_basic ( ) {
1209+ let dir = TempDir :: new ( ) ;
1210+ let path = dir. write (
1211+ "firefox.desktop" ,
1212+ "[Desktop Entry]\n \
1213+ Type=Application\n \
1214+ Name=Firefox\n \
1215+ GenericName=Web Browser\n \
1216+ Comment=Browse the web\n \
1217+ Exec=/usr/bin/firefox\n \
1218+ Icon=firefox\n \
1219+ Keywords=internet;browser;\n \
1220+ Terminal=false\n ",
1221+ ) ;
1222+
1223+ let entry = ApplicationsProvider :: parse_desktop_file ( & path) . expect ( "should parse" ) ;
1224+ assert_eq ! ( entry. id, "firefox" ) ;
1225+ assert_eq ! ( entry. name, "Firefox" ) ;
1226+ assert_eq ! ( entry. generic_name. as_deref( ) , Some ( "Web Browser" ) ) ;
1227+ assert_eq ! ( entry. comment. as_deref( ) , Some ( "Browse the web" ) ) ;
1228+ assert_eq ! ( entry. icon, "firefox" ) ;
1229+ assert ! ( entry. keywords. iter( ) . any( |k| k == "browser" ) ) ;
1230+ assert ! ( !entry. terminal) ;
1231+ // Icon resolution is deferred - parse leaves it unset.
1232+ assert ! ( entry. icon_path. is_none( ) ) ;
1233+ }
1234+
1235+ #[ test]
1236+ fn parse_desktop_file_skips_nodisplay_and_no_exec ( ) {
1237+ let dir = TempDir :: new ( ) ;
1238+
1239+ let hidden = dir. write (
1240+ "hidden.desktop" ,
1241+ "[Desktop Entry]\n Type=Application\n Name=Hidden\n Exec=/bin/true\n NoDisplay=true\n " ,
1242+ ) ;
1243+ assert ! ( ApplicationsProvider :: parse_desktop_file( & hidden) . is_none( ) ) ;
1244+
1245+ let no_exec = dir. write (
1246+ "noexec.desktop" ,
1247+ "[Desktop Entry]\n Type=Application\n Name=NoExec\n " ,
1248+ ) ;
1249+ assert ! ( ApplicationsProvider :: parse_desktop_file( & no_exec) . is_none( ) ) ;
1250+ }
1251+
1252+ #[ test]
1253+ fn load_applications_into_reads_extra_dir ( ) {
1254+ let dir = TempDir :: new ( ) ;
1255+ let unique = "datacube-unit-test-app-xyz" ;
1256+ dir. write (
1257+ & format ! ( "{unique}.desktop" ) ,
1258+ "[Desktop Entry]\n Type=Application\n Name=Datacube Unit Test App\n Exec=/bin/true\n Icon=x\n " ,
1259+ ) ;
1260+
1261+ let apps = Arc :: new ( RwLock :: new ( HashMap :: new ( ) ) ) ;
1262+ let path_to_id = Arc :: new ( RwLock :: new ( HashMap :: new ( ) ) ) ;
1263+ ApplicationsProvider :: load_applications_into ( & apps, & path_to_id, & [ dir. path . clone ( ) ] ) ;
1264+
1265+ let guard = apps. read ( ) . unwrap ( ) ;
1266+ let entry = guard. get ( unique) . expect ( "temp app should be loaded" ) ;
1267+ assert_eq ! ( entry. name, "Datacube Unit Test App" ) ;
1268+ }
1269+
1270+ #[ test]
1271+ fn query_matches_by_name ( ) {
1272+ let provider = provider_with ( vec ! [
1273+ make_entry( "firefox" , "Firefox" ) ,
1274+ make_entry( "gimp" , "GIMP" ) ,
1275+ make_entry( "code" , "Visual Studio Code" ) ,
1276+ ] ) ;
1277+
1278+ let results = provider. query_impl ( "firefox" , 10 ) ;
1279+ assert ! ( !results. is_empty( ) ) ;
1280+ assert_eq ! ( results[ 0 ] . text, "Firefox" ) ;
1281+ assert_eq ! ( results[ 0 ] . provider, "applications" ) ;
1282+ }
1283+
1284+ #[ test]
1285+ fn query_matches_by_id ( ) {
1286+ let mut entry = make_entry ( "org.mozilla.firefox" , "Firefox" ) ;
1287+ entry. source = AppSource :: Flatpak ;
1288+ let provider = provider_with ( vec ! [ entry, make_entry( "gimp" , "GIMP" ) ] ) ;
1289+
1290+ let results = provider. query_impl ( "mozilla" , 10 ) ;
1291+ assert_eq ! ( results. len( ) , 1 ) ;
1292+ assert_eq ! ( results[ 0 ] . text, "Firefox" ) ;
1293+ }
1294+
1295+ #[ test]
1296+ fn query_no_match_is_empty ( ) {
1297+ let provider = provider_with ( vec ! [ make_entry( "firefox" , "Firefox" ) ] ) ;
1298+ assert ! ( provider. query_impl( "zzzzzznotanapp" , 10 ) . is_empty( ) ) ;
1299+ }
1300+
1301+ #[ test]
1302+ fn query_empty_returns_all_up_to_max ( ) {
1303+ let provider = provider_with ( vec ! [
1304+ make_entry( "a" , "Alpha" ) ,
1305+ make_entry( "b" , "Beta" ) ,
1306+ make_entry( "c" , "Gamma" ) ,
1307+ ] ) ;
1308+
1309+ assert_eq ! ( provider. query_impl( "" , 10 ) . len( ) , 3 ) ;
1310+ assert_eq ! ( provider. query_impl( "" , 2 ) . len( ) , 2 ) ;
1311+ }
1312+ }
0 commit comments