[vlc-commits] [Git][videolan/vlc][master] 3 commits: macosx: Stop media-library callbacks before cold-count tests

Steve Lhomme (@robUx4) gitlab at videolan.org
Tue Nov 3 08:04:04 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
3d436805 by Claudio Cambra at 2026-11-03T07:42:24+00:00
macosx: Stop media-library callbacks before cold-count tests

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
9b62470c by Claudio Cambra at 2026-11-03T07:42:24+00:00
macosx: Check stable identities in media-library deletion tests

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
5c8ca8c4 by Claudio Cambra at 2026-11-03T07:42:24+00:00
macosx: Run media-library fixture SQL in a separate process

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -


5 changed files:

- modules/gui/macosx/tests/Makefile.am
- modules/gui/macosx/tests/VLCLibraryDataTypesIntegrationTestSupport.m
- + modules/gui/macosx/tests/VLCLibraryFixtureSQL.m
- modules/gui/macosx/tests/VLCLibraryModelConcurrencyTest.m
- modules/gui/macosx/tests/VLCLibraryModelIntegrationTest.m


Changes:

=====================================
modules/gui/macosx/tests/Makefile.am
=====================================
@@ -5,7 +5,7 @@ if ENABLE_MACOSX_XCTESTS
 macosx_gui_xctest_frameworks_dir = $(shell $(XCRUN) --sdk macosx --show-sdk-platform-path)/Developer/Library/Frameworks
 macosx_gui_xctest_bundles = VLCMacOSXTests.xctest
 
-check_PROGRAMS += VLCMacOSXTests
+check_PROGRAMS += VLCMacOSXTests VLCLibraryFixtureSQL
 TESTS += $(macosx_gui_xctest_bundles)
 
 VLCMacOSXTests_SOURCES = \
@@ -105,20 +105,27 @@ VLCMacOSXTests_LDFLAGS = \
 
 VLCMacOSXTests_LDADD = \
 	$(top_builddir)/lib/libvlc.la \
-	$(top_builddir)/src/libvlccore.la \
-	-lsqlite3
+	$(top_builddir)/src/libvlccore.la
+
+VLCLibraryFixtureSQL_SOURCES = gui/macosx/tests/VLCLibraryFixtureSQL.m
+VLCLibraryFixtureSQL_OBJCFLAGS = $(AM_OBJCFLAGS) -fobjc-arc -fobjc-exceptions -fmodules
+VLCLibraryFixtureSQL_LDFLAGS = -Wl,-framework,Foundation
+VLCLibraryFixtureSQL_LDADD = -lsqlite3
 
 VLCMacOSXTests_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
 	$(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(OBJCLD) \
 	$(VLCMacOSXTests_OBJCFLAGS) $(OBJCFLAGS) \
 	$(VLCMacOSXTests_LDFLAGS) $(LDFLAGS) -o $@
 
-VLCMacOSXTests.xctest: VLCMacOSXTests$(EXEEXT) $(srcdir)/gui/macosx/tests/VLCMacOSXTests-Info.plist
+VLCMacOSXTests.xctest: VLCMacOSXTests$(EXEEXT) VLCLibraryFixtureSQL$(EXEEXT) $(srcdir)/gui/macosx/tests/VLCMacOSXTests-Info.plist
 	$(AM_V_GEN)rm -rf $@
 	$(AM_V_at)$(MKDIR_P) $@/Contents/MacOS
 	$(AM_V_at)$(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install \
 		$(INSTALL_PROGRAM) VLCMacOSXTests$(EXEEXT) \
 		$(abs_builddir)/$@/Contents/MacOS/VLCMacOSXTests
+	$(AM_V_at)$(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install \
+		$(INSTALL_PROGRAM) VLCLibraryFixtureSQL$(EXEEXT) \
+		$(abs_builddir)/$@/Contents/MacOS/VLCLibraryFixtureSQL
 	$(AM_V_at)$(INSTALL_DATA) $(srcdir)/gui/macosx/tests/VLCMacOSXTests-Info.plist $@/Contents/Info.plist
 
 EXTRA_DIST += gui/macosx/tests/VLCMacOSXTests-Info.plist


=====================================
modules/gui/macosx/tests/VLCLibraryDataTypesIntegrationTestSupport.m
=====================================
@@ -16,9 +16,10 @@
 #import <vlc/libvlc.h>
 
 #include <string.h>
-#include <ctype.h>
+#include <stdlib.h>
 #include <dispatch/dispatch.h>
-#include <sqlite3.h>
+#include <signal.h>
+#include <fcntl.h>
 #include <unistd.h>
 
 #import "VLCLibraryDataTypesIntegrationTestSupport.h"
@@ -52,62 +53,97 @@ static int64_t sGroupID;
 static BOOL sFactoryFixturesPrepared;
 static NSString *sUserDataPath;
 
-// Match medialibrary 0.13.2's sqlite::Connection::collateFilename, including
-// its leading-number comparison and length-bounded case-insensitive suffix.
-// Adapted from SqliteConnection.cpp.
-static int VLCLibraryDataTypesIntegrationCompareFilenames(void * __unused context,
-                                                          int lhsSize,
-                                                          const void *lhs,
-                                                          int rhsSize,
-                                                          const void *rhs)
+// The media-library plugin has its own SQLite copy. Run fixture SQL in another
+// process so SQLite's process-wide file locks keep the two clients coordinated.
+static NSDictionary *VLCLibraryDataTypesIntegrationSQLStatement(NSString *sql,
+                                                               NSArray *parameters,
+                                                               NSUInteger minimumChanges)
 {
-    const char *left = lhs;
-    const char *right = rhs;
-    int size = MIN(lhsSize, rhsSize);
-    if (size == 0)
-        return lhsSize - rhsSize;
-    if (!isdigit((unsigned char)*left) || !isdigit((unsigned char)*right))
-        return sqlite3_strnicmp(left, right, size);
-
-    int leftNumber = 0;
-    while (lhsSize > 0 && isdigit((unsigned char)*left)) {
-        leftNumber = leftNumber * 10 + (*left++ - '0');
-        lhsSize--;
-    }
-    int rightNumber = 0;
-    while (rhsSize > 0 && isdigit((unsigned char)*right)) {
-        rightNumber = rightNumber * 10 + (*right++ - '0');
-        rhsSize--;
-    }
-    if (leftNumber != rightNumber)
-        return leftNumber - rightNumber;
-    return sqlite3_strnicmp(left, right, MIN(lhsSize, rhsSize));
+    return @{ @"sql": sql, @"parameters": parameters, @"minimumChanges": @(minimumChanges) };
 }
 
-static BOOL VLCLibraryDataTypesIntegrationOpenDatabase(sqlite3 **database)
+static BOOL VLCLibraryDataTypesIntegrationExecuteStatements(NSArray<NSDictionary *> *statements)
 {
-    NSString * const path = [sUserDataPath stringByAppendingPathComponent:@"ml/ml.db"];
-    BOOL success = sqlite3_open_v2(path.fileSystemRepresentation,
-                                   database,
-                                   SQLITE_OPEN_READWRITE,
-                                   NULL) == SQLITE_OK;
-    if (success) {
-        success = sqlite3_busy_timeout(*database, 5000) == SQLITE_OK &&
-            sqlite3_create_collation_v2(*database,
-                                        "FILENAME",
-                                        SQLITE_UTF8,
-                                        NULL,
-                                        VLCLibraryDataTypesIntegrationCompareFilenames,
-                                        NULL) == SQLITE_OK &&
-            sqlite3_exec(*database, "PRAGMA foreign_keys = ON", NULL, NULL, NULL) == SQLITE_OK &&
-            sqlite3_exec(*database, "PRAGMA recursive_triggers = ON", NULL, NULL, NULL) == SQLITE_OK;
-    }
-    if (!success && *database != NULL) {
-        NSLog(@"Could not open fixture database: %s", sqlite3_errmsg(*database));
-        sqlite3_close(*database);
-        *database = NULL;
-    }
-    return success;
+    NSString * const databasePath = [sUserDataPath stringByAppendingPathComponent:@"ml/ml.db"];
+    NSString * const helperPath = [[[[NSBundle bundleForClass:VLCInputItem.class] executablePath]
+        stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"VLCLibraryFixtureSQL"];
+    NSError *error = nil;
+    NSData * const request = [NSJSONSerialization dataWithJSONObject:
+        @{ @"database": databasePath, @"statements": statements }
+        options:0 error:&error];
+    if (request == nil) {
+        NSLog(@"Could not encode fixture SQL request: %@", error);
+        return NO;
+    }
+
+    NSTask * const task = [[NSTask alloc] init];
+    task.executableURL = [NSURL fileURLWithPath:helperPath];
+    NSPipe * const input = NSPipe.pipe;
+    NSPipe * const output = NSPipe.pipe;
+    task.standardInput = input;
+    task.standardOutput = output;
+    // A child killed at the deadline must not send SIGPIPE to the test runner.
+    if (fcntl(input.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1) < 0) {
+        [input.fileHandleForWriting closeFile];
+        [output.fileHandleForReading closeFile];
+        NSLog(@"Could not configure fixture SQL input pipe");
+        return NO;
+    }
+    // The child reports one small JSON result; diagnostics inherit stderr.
+    dispatch_semaphore_t const finished = dispatch_semaphore_create(0);
+    task.terminationHandler = ^(NSTask * __unused terminatedTask) {
+        dispatch_semaphore_signal(finished);
+    };
+    if (![task launchAndReturnError:&error]) {
+        [input.fileHandleForWriting closeFile];
+        [output.fileHandleForReading closeFile];
+        NSLog(@"Could not launch fixture SQL helper: %@", error);
+        return NO;
+    }
+
+    dispatch_group_t const io = dispatch_group_create();
+    dispatch_queue_t const queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
+    __block NSString *writeError = nil;
+    __block NSString *readError = nil;
+    __block NSData *responseData = nil;
+    dispatch_group_async(io, queue, ^{
+        @try {
+            [input.fileHandleForWriting writeData:request];
+        } @catch (NSException *exception) {
+            writeError = exception.reason;
+        } @finally {
+            [input.fileHandleForWriting closeFile];
+        }
+    });
+    dispatch_group_async(io, queue, ^{
+        @try {
+            responseData = [output.fileHandleForReading readDataToEndOfFile];
+        } @catch (NSException *exception) {
+            readError = exception.reason;
+        } @finally {
+            [output.fileHandleForReading closeFile];
+        }
+    });
+    const BOOL timedOut = dispatch_semaphore_wait(finished,
+        dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)) != 0;
+    if (timedOut && task.running)
+        kill(task.processIdentifier, SIGKILL);
+    // Reap the child and finish pipe I/O before the fixture database is removed.
+    [task waitUntilExit];
+    dispatch_group_wait(io, DISPATCH_TIME_FOREVER);
+    if (timedOut || writeError != nil || readError != nil) {
+        NSLog(@"Fixture SQL transport failed: %@", timedOut
+            ? @"helper exceeded its 15-second deadline" : writeError ?: readError);
+        return NO;
+    }
+    id const response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
+    if (task.terminationStatus != EXIT_SUCCESS ||
+        ![response isKindOfClass:NSDictionary.class] || ![response[@"success"] boolValue]) {
+        NSLog(@"Fixture SQL helper failed (exit %d): %@", task.terminationStatus,
+              [response isKindOfClass:NSDictionary.class] ? response[@"error"] : error);
+        return NO;
+    }
+    return YES;
 }
 
 typedef struct
@@ -137,82 +173,28 @@ static BOOL VLCLibraryDataTypesIntegrationMediaMatchesMRL(const vlc_ml_media_t *
 
 static BOOL VLCLibraryDataTypesIntegrationSetMovieMetadata(int64_t mediaID)
 {
-    /* The VLC medialibrary bridge exposes movie fields as read-only. Seed the
-     * persisted media and movie rows so the wrapper conversion is tested with
-     * a real video-typed movie and non-empty metadata. */
-    sqlite3 *database = NULL;
-    if (!VLCLibraryDataTypesIntegrationOpenDatabase(&database))
-        return NO;
-
-    sqlite3_stmt *statement = NULL;
-    BOOL success = sqlite3_prepare_v2(database,
-                                      "UPDATE Media SET type = ?, subtype = ? "
-                                      "WHERE id_media = ?",
-                                      -1,
-                                      &statement,
-                                      NULL) == SQLITE_OK;
-    if (success) {
-        sqlite3_bind_int(statement, 1, VLC_ML_MEDIA_TYPE_VIDEO);
-        sqlite3_bind_int(statement, 2, VLC_ML_MEDIA_SUBTYPE_MOVIE);
-        sqlite3_bind_int64(statement, 3, mediaID);
-        success = sqlite3_step(statement) == SQLITE_DONE;
-    }
-    sqlite3_finalize(statement);
-
-    if (success) {
-        success = sqlite3_prepare_v2(database,
-                                      "DELETE FROM Movie WHERE media_id = ?",
-                                      -1,
-                                      &statement,
-                                      NULL) == SQLITE_OK;
-    }
-    if (success) {
-        sqlite3_bind_int64(statement, 1, mediaID);
-        success = sqlite3_step(statement) == SQLITE_DONE;
-    }
-    sqlite3_finalize(statement);
-
-    if (success) {
-        success = sqlite3_prepare_v2(database,
-                                      "INSERT INTO Movie (media_id, summary, imdb_id) "
-                                      "VALUES (?, ?, ?)",
-                                      -1,
-                                      &statement,
-                                      NULL) == SQLITE_OK;
-    }
-    if (success) {
-        sqlite3_bind_int64(statement, 1, mediaID);
-        sqlite3_bind_text(statement, 2, "Factory movie summary", -1, SQLITE_STATIC);
-        sqlite3_bind_text(statement, 3, "tt1234567", -1, SQLITE_STATIC);
-        success = sqlite3_step(statement) == SQLITE_DONE;
-    }
-    sqlite3_finalize(statement);
-    sqlite3_close(database);
-    return success;
+    // The VLC bridge exposes movie fields as read-only. Seed the related rows
+    // atomically so media-library readers cannot observe a partly converted movie.
+    return VLCLibraryDataTypesIntegrationExecuteStatements(@[
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"UPDATE Media SET type = ?1, subtype = ?2 WHERE id_media = ?3",
+            @[ @(VLC_ML_MEDIA_TYPE_VIDEO), @(VLC_ML_MEDIA_SUBTYPE_MOVIE), @(mediaID) ], 1),
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"DELETE FROM Movie WHERE media_id = ?1", @[ @(mediaID) ], 0),
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"INSERT INTO Movie (media_id, summary, imdb_id) VALUES (?1, ?2, ?3)",
+            @[ @(mediaID), @"Factory movie summary", @"tt1234567" ], 1),
+    ]);
 }
 
 static BOOL VLCLibraryDataTypesIntegrationSetMediaType(int64_t mediaID,
                                                        int type)
 {
-    sqlite3 *database = NULL;
-    if (!VLCLibraryDataTypesIntegrationOpenDatabase(&database))
-        return NO;
-
-    sqlite3_stmt *statement = NULL;
-    BOOL success = sqlite3_prepare_v2(database,
-                                      "UPDATE Media SET type = ?, import_type = 0, "
-                                      "is_present = 1 WHERE id_media = ?",
-                                      -1,
-                                      &statement,
-                                      NULL) == SQLITE_OK;
-    if (success) {
-        sqlite3_bind_int(statement, 1, type);
-        sqlite3_bind_int64(statement, 2, mediaID);
-        success = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(database) == 1;
-    }
-    sqlite3_finalize(statement);
-    sqlite3_close(database);
-    return success;
+    return VLCLibraryDataTypesIntegrationExecuteStatements(@[
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"UPDATE Media SET type = ?2, import_type = 0, is_present = 1 WHERE id_media = ?1",
+            @[ @(mediaID), @(type) ], 1),
+    ]);
 }
 
 static void VLCLibraryDataTypesIntegrationMediaLibraryEvent(void *data,
@@ -888,24 +870,10 @@ static BOOL VLCLibraryDataTypesIntegrationExecuteEntityStatement(const char *sql
                                                                  int64_t libraryID,
                                                                  id value)
 {
-    sqlite3 *database = NULL;
-    if (!VLCLibraryDataTypesIntegrationOpenDatabase(&database))
-        return NO;
-    sqlite3_stmt *statement = NULL;
-    BOOL success = sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK;
-    if (success) {
-        sqlite3_bind_int64(statement, 1, libraryID);
-        if ([value isKindOfClass:NSNumber.class])
-            sqlite3_bind_int64(statement, 2, [value longLongValue]);
-        else if ([value isKindOfClass:NSString.class])
-            sqlite3_bind_text(statement, 2, [value UTF8String], -1, SQLITE_TRANSIENT);
-        success = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(database) > 0;
-    }
-    if (!success)
-        NSLog(@"Could not modify fixture entity %lld: %s", libraryID, sqlite3_errmsg(database));
-    sqlite3_finalize(statement);
-    sqlite3_close(database);
-    return success;
+    NSArray * const parameters = value == nil ? @[ @(libraryID) ] : @[ @(libraryID), value ];
+    return VLCLibraryDataTypesIntegrationExecuteStatements(@[
+        VLCLibraryDataTypesIntegrationSQLStatement([NSString stringWithUTF8String:sql], parameters, 1),
+    ]);
 }
 
 int64_t VLCLibraryDataTypesIntegrationCreateExternalMediaOfType(const char *mrl, vlc_ml_media_type_t type)
@@ -1057,37 +1025,16 @@ BOOL VLCLibraryDataTypesIntegrationMoveMediaToAlbum(int64_t mediaID, int64_t alb
 
 BOOL VLCLibraryDataTypesIntegrationMoveMediaToArtist(int64_t mediaID, int64_t artistID)
 {
-    sqlite3 *database = NULL;
-    if (!VLCLibraryDataTypesIntegrationOpenDatabase(&database))
-        return NO;
-    BOOL success = sqlite3_exec(database, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK;
-    // The relation-table delete/insert triggers maintain both artists' real
-    // track counts. Keep the media's primary artist and relation consistent.
-    const char * const statements[] = {
-        "DELETE FROM MediaArtistRelation WHERE media_id = ?1",
-        "UPDATE Media SET artist_id = ?2 WHERE id_media = ?1",
-        "INSERT INTO MediaArtistRelation (media_id, artist_id) VALUES (?1, ?2)",
-    };
-    for (NSUInteger index = 0; success && index < sizeof(statements) / sizeof(statements[0]); index++) {
-        sqlite3_stmt *statement = NULL;
-        success = sqlite3_prepare_v2(database, statements[index], -1, &statement, NULL) == SQLITE_OK;
-        if (success) {
-            success = sqlite3_bind_int64(statement, 1, mediaID) == SQLITE_OK;
-            if (success && sqlite3_bind_parameter_count(statement) > 1)
-                success = sqlite3_bind_int64(statement, 2, artistID) == SQLITE_OK;
-            if (success)
-                success = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(database) > 0;
-        }
-        sqlite3_finalize(statement);
-    }
-    if (success)
-        success = sqlite3_exec(database, "COMMIT", NULL, NULL, NULL) == SQLITE_OK;
-    if (!success) {
-        NSLog(@"Could not move fixture media %lld to artist %lld: %s", mediaID, artistID, sqlite3_errmsg(database));
-        sqlite3_exec(database, "ROLLBACK", NULL, NULL, NULL);
-    }
-    sqlite3_close(database);
-    return success;
+    // Keep the primary artist and trigger-maintained relation counts consistent.
+    return VLCLibraryDataTypesIntegrationExecuteStatements(@[
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"DELETE FROM MediaArtistRelation WHERE media_id = ?1", @[ @(mediaID) ], 1),
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"UPDATE Media SET artist_id = ?2 WHERE id_media = ?1", @[ @(mediaID), @(artistID) ], 1),
+        VLCLibraryDataTypesIntegrationSQLStatement(
+            @"INSERT INTO MediaArtistRelation (media_id, artist_id) VALUES (?1, ?2)",
+            @[ @(mediaID), @(artistID) ], 1),
+    ]);
 }
 
 BOOL VLCLibraryDataTypesIntegrationSetMovieSummary(int64_t mediaID, NSString *summary)


=====================================
modules/gui/macosx/tests/VLCLibraryFixtureSQL.m
=====================================
@@ -0,0 +1,161 @@
+/****************************************************************************
+ * VLCLibraryFixtureSQL.m: isolated SQL mutations for medialibrary tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version.
+ *****************************************************************************/
+
+#import <Foundation/Foundation.h>
+
+#include <ctype.h>
+#include <sqlite3.h>
+#include <stdlib.h>
+
+// Match medialibrary's FILENAME collation, including leading-number comparison.
+static int CompareFilenames(void * __unused context, int lhsSize, const void *lhs,
+                            int rhsSize, const void *rhs)
+{
+    const char *left = lhs;
+    const char *right = rhs;
+    int size = MIN(lhsSize, rhsSize);
+    if (size == 0)
+        return lhsSize - rhsSize;
+    if (!isdigit((unsigned char)*left) || !isdigit((unsigned char)*right))
+        return sqlite3_strnicmp(left, right, size);
+
+    int leftNumber = 0;
+    while (lhsSize > 0 && isdigit((unsigned char)*left)) {
+        leftNumber = leftNumber * 10 + (*left++ - '0');
+        lhsSize--;
+    }
+    int rightNumber = 0;
+    while (rhsSize > 0 && isdigit((unsigned char)*right)) {
+        rightNumber = rightNumber * 10 + (*right++ - '0');
+        rhsSize--;
+    }
+    if (leftNumber != rightNumber)
+        return leftNumber - rightNumber;
+    return sqlite3_strnicmp(left, right, MIN(lhsSize, rhsSize));
+}
+
+static BOOL ExecuteStatement(sqlite3 *database, NSDictionary *request, NSString **error)
+{
+    NSString * const sql = request[@"sql"];
+    NSArray * const parameters = request[@"parameters"];
+    NSNumber * const minimumChanges = request[@"minimumChanges"];
+    if (![sql isKindOfClass:NSString.class] ||
+        ![parameters isKindOfClass:NSArray.class] ||
+        ![minimumChanges isKindOfClass:NSNumber.class]) {
+        *error = @"Invalid fixture SQL statement request";
+        return NO;
+    }
+    sqlite3_stmt *statement = NULL;
+    BOOL success = sqlite3_prepare_v2(database, sql.UTF8String, -1, &statement, NULL) == SQLITE_OK;
+    if (success && (NSUInteger)sqlite3_bind_parameter_count(statement) != parameters.count) {
+        *error = @"Fixture SQL parameter count does not match the statement";
+        success = NO;
+    }
+    for (NSUInteger index = 0; success && index < parameters.count; index++) {
+        id const value = parameters[index];
+        int result;
+        if ([value isKindOfClass:NSNumber.class])
+            result = sqlite3_bind_int64(statement, (int)index + 1, [value longLongValue]);
+        else if ([value isKindOfClass:NSString.class])
+            result = sqlite3_bind_text(statement, (int)index + 1, [value UTF8String], -1, SQLITE_TRANSIENT);
+        else if (value == NSNull.null)
+            result = sqlite3_bind_null(statement, (int)index + 1);
+        else {
+            *error = @"Unsupported fixture SQL parameter type";
+            success = NO;
+            break;
+        }
+        success = result == SQLITE_OK;
+    }
+    if (success)
+        success = sqlite3_step(statement) == SQLITE_DONE;
+    if (success && sqlite3_changes(database) < minimumChanges.longLongValue) {
+        *error = [NSString stringWithFormat:@"Fixture SQL changed %d rows, expected at least %@",
+                  sqlite3_changes(database), minimumChanges];
+        success = NO;
+    }
+    if (!success && *error == nil)
+        *error = [NSString stringWithUTF8String:sqlite3_errmsg(database)];
+    sqlite3_finalize(statement);
+    return success;
+}
+
+static BOOL ExecuteRequest(NSDictionary *request, NSString **error)
+{
+    NSString * const path = request[@"database"];
+    NSArray * const statements = request[@"statements"];
+    if (![path isKindOfClass:NSString.class] || path.length == 0 ||
+        ![statements isKindOfClass:NSArray.class] || statements.count == 0) {
+        *error = @"Invalid fixture SQL request";
+        return NO;
+    }
+    sqlite3 *database = NULL;
+    BOOL success = sqlite3_open_v2(path.fileSystemRepresentation, &database,
+                                 SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK;
+    if (success) {
+        success = sqlite3_busy_timeout(database, 5000) == SQLITE_OK &&
+            sqlite3_create_collation_v2(database, "FILENAME", SQLITE_UTF8, NULL,
+                                        CompareFilenames, NULL) == SQLITE_OK &&
+            sqlite3_exec(database, "PRAGMA foreign_keys = ON", NULL, NULL, NULL) == SQLITE_OK &&
+            sqlite3_exec(database, "PRAGMA recursive_triggers = ON", NULL, NULL, NULL) == SQLITE_OK &&
+            sqlite3_exec(database, "BEGIN IMMEDIATE", NULL, NULL, NULL) == SQLITE_OK;
+    }
+    for (NSUInteger index = 0; success && index < statements.count; index++) {
+        id const statement = statements[index];
+        if (![statement isKindOfClass:NSDictionary.class]) {
+            *error = @"Invalid fixture SQL statement request";
+            success = NO;
+        } else {
+            success = ExecuteStatement(database, statement, error);
+        }
+        if (!success)
+            *error = [NSString stringWithFormat:@"Statement %lu: %@", (unsigned long)index + 1, *error];
+    }
+    if (success)
+        success = sqlite3_exec(database, "COMMIT", NULL, NULL, NULL) == SQLITE_OK;
+    if (!success) {
+        if (*error == nil)
+            *error = database == NULL ? @"Could not open fixture database" :
+                [NSString stringWithUTF8String:sqlite3_errmsg(database)];
+        if (database != NULL)
+            sqlite3_exec(database, "ROLLBACK", NULL, NULL, NULL);
+    }
+    if (database != NULL) {
+        const int result = sqlite3_close(database);
+        if (success && result != SQLITE_OK) {
+            *error = @"Could not close fixture database";
+            success = NO;
+        }
+    }
+    return success;
+}
+
+int main(void)
+{
+    @autoreleasepool {
+        NSError *parseError = nil;
+        NSData * const data = [NSFileHandle.fileHandleWithStandardInput readDataToEndOfFile];
+        id const request = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
+        NSString *error = nil;
+        BOOL success = NO;
+        if ([request isKindOfClass:NSDictionary.class])
+            success = ExecuteRequest(request, &error);
+        else
+            error = parseError.localizedDescription ?: @"Invalid fixture SQL request";
+        NSDictionary * const response = success ? @{ @"success": @YES } :
+            @{ @"success": @NO, @"error": error ?: @"Fixture SQL operation failed" };
+        NSData * const responseData = [NSJSONSerialization dataWithJSONObject:response options:0 error:NULL];
+        [NSFileHandle.fileHandleWithStandardOutput writeData:responseData];
+        return success ? EXIT_SUCCESS : EXIT_FAILURE;
+    }
+}


=====================================
modules/gui/macosx/tests/VLCLibraryModelConcurrencyTest.m
=====================================
@@ -1916,6 +1916,22 @@ typedef NS_ENUM(NSUInteger, VLCLibraryModelOverlapChange) {
         NSNotificationName const recentDeletion = audio ? VLCLibraryModelRecentAudioMediaItemDeleted : VLCLibraryModelRecentsMediaItemDeleted;
         [changes addObject:[self expectChangesNamed:mainDeletion IDs:@[ @(pendingID) ]]];
         [changes addObject:[self expectChangesNamed:recentDeletion IDs:@[ @(pendingID) ]]];
+        // Controlled event delivery keeps these seeded snapshots stable until deletion.
+        NSArray * const deletedItems = @[
+            [self itemWithID:pendingID inItems:self.fixture.cachedItems],
+            [self itemWithID:pendingID inItems:[self recentItemsForKind:kind]],
+        ];
+        NSArray<NSNotificationName> * const deletionNames = @[ mainDeletion, recentDeletion ];
+        [deletionNames enumerateObjectsUsingBlock:^(NSNotificationName name, NSUInteger index, BOOL * __unused stop) {
+            id const cachedItem = deletedItems[index];
+            [self observeNotification:name
+                               object:nil
+                                queue:NSOperationQueue.mainQueue
+                           usingBlock:^(NSNotification *notification) {
+                if ([notification.object libraryID] == pendingID)
+                    XCTAssertEqual(notification.object, cachedItem);
+            }];
+        }];
     }
     XCTestExpectation * const queueHeld = [self expectationWithDescription:@"Shared media-cache queue held"];
     [model holdWritesFollowingNextCacheReadWithHandler:^{


=====================================
modules/gui/macosx/tests/VLCLibraryModelIntegrationTest.m
=====================================
@@ -1124,7 +1124,14 @@
                          counter:(size_t (^)(void))counter
                           loader:(NSArray *(^)(void))loader
 {
+    // Stop media-library callbacks after lazy construction and drain submitted work.
+    (void)self.model;
     [self stopModelCallback];
+    XCTestExpectation * const settled = [self expectationWithDescription:@"Media-library callbacks settled before filtered count query"];
+    [self.model finishPendingQueriesWithCompletion:^{
+        [settled fulfill];
+    }];
+    [self waitForExpectations:@[ settled ] timeout:10.];
     const struct {
         SEL getter;
         __unsafe_unretained NSNotificationName notification;
@@ -1377,7 +1384,10 @@
             [notification.object libraryID] != libraryID)
             return NO;
         XCTAssertTrue(NSThread.isMainThread);
-        XCTAssertEqual(notification.object, previous);
+        VLCMediaLibraryMediaItem * const deletedItem = notification.object;
+        // Media-library callbacks may replace the snapshot before deletion arrives.
+        XCTAssertEqual(deletedItem.libraryID, previous.libraryID);
+        XCTAssertEqual(deletedItem.mediaType, type);
         NSArray * const cache = audio
             ? self.model.listOfRecentAudioMedia
             : self.model.listOfRecentMedia;



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/9697b3130204ca1782b08f8c5bfdfd1390aab811...5c8ca8c49a606d4a59553b9f77dfdf78da241037

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/9697b3130204ca1782b08f8c5bfdfd1390aab811...5c8ca8c49a606d4a59553b9f77dfdf78da241037
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list