[vlc-commits] [Git][videolan/vlc][master] 13 commits: macosx: Add test for VLCHexNumberFormatterTest
Steve Lhomme (@robUx4)
gitlab at videolan.org
Wed Aug 26 07:48:21 UTC 2026
Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits:
887ac91b by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add test for VLCHexNumberFormatterTest
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
5dc7d2b1 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Fix time formatter string object -> number calc
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
39b81c3a by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add VLCTimeFormatter test
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
cab7dd1b by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add NSString+Helpers tests
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
5b61e724 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Fix time parsing for exactly 3600 seconds
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
ce380c02 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add log message test
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
e7471e9b by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Check for info validity before access in log message
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
c30bda26 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add basic initial test for VLCInputItem
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
c6173ddc by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add input node test
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
7626a3ee by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Extract VLCInputNode to own header/implementation files
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
6ddac818 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Add string equality check that also matches nullability
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
6ab28918 by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Use nil-inclusive equality check for input item state differentiation check
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
44c4002e by Claudio Cambra at 2026-08-26T07:32:26+00:00
macosx: Expand input item testing (parsing, notifications, thumbnail states, differing states)
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
25 changed files:
- modules/gui/macosx/Makefile.am
- modules/gui/macosx/extensions/NSString+Helpers.h
- modules/gui/macosx/extensions/NSString+Helpers.m
- modules/gui/macosx/library/VLCInputItem.h
- modules/gui/macosx/library/VLCInputItem.m
- + modules/gui/macosx/library/VLCInputNode.h
- + modules/gui/macosx/library/VLCInputNode.m
- modules/gui/macosx/library/VLCInputNodePathControlItem.m
- modules/gui/macosx/library/VLCLibrarySegment.m
- modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationStack.m
- modules/gui/macosx/library/media-source/VLCLocalMediaSource.m
- modules/gui/macosx/library/media-source/VLCMediaSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
- modules/gui/macosx/tests/Makefile.am
- + modules/gui/macosx/tests/NSStringHelpersTest.m
- + modules/gui/macosx/tests/VLCHexNumberFormatterTest.m
- + modules/gui/macosx/tests/VLCInputItemTest.m
- + modules/gui/macosx/tests/VLCInputItemTestSupport.h
- + modules/gui/macosx/tests/VLCInputItemTestSupport.m
- + modules/gui/macosx/tests/VLCInputNodeTest.m
- + modules/gui/macosx/tests/VLCLogMessageTest.m
- + modules/gui/macosx/tests/VLCTimeFormatterTest.m
- modules/gui/macosx/views/VLCTimeFormatter.m
- modules/gui/macosx/windows/logging/VLCLogMessage.m
Changes:
=====================================
modules/gui/macosx/Makefile.am
=====================================
@@ -147,6 +147,8 @@ libmacosx_plugin_la_SOURCES = \
gui/macosx/imported/SPMediaKeyTap/SPMediaKeyTap.m \
gui/macosx/library/VLCInputItem.h \
gui/macosx/library/VLCInputItem.m \
+ gui/macosx/library/VLCInputNode.h \
+ gui/macosx/library/VLCInputNode.m \
gui/macosx/library/VLCInputNodePathControl.h \
gui/macosx/library/VLCInputNodePathControl.m \
gui/macosx/library/VLCInputNodePathControlItem.h \
=====================================
modules/gui/macosx/extensions/NSString+Helpers.h
=====================================
@@ -74,6 +74,12 @@ bool fixIntfSettings(void);
@interface NSString (Helpers)
+/**
+ Compares two strings by value, treating two nil strings as equal.
+ */
++ (BOOL)equalOrBothNil:(nullable NSString *)firstString
+ to:(nullable NSString *)secondString;
+
/**
Creates an NSString with the current time of the \c input_item_t
=====================================
modules/gui/macosx/extensions/NSString+Helpers.m
=====================================
@@ -79,6 +79,16 @@ NSString * flagEmojiStringForCountryCode(NSString *countryCode)
@implementation NSString (Helpers)
++ (BOOL)equalOrBothNil:(nullable NSString *)firstString
+ to:(nullable NSString *)secondString
+{
+ if (firstString == nil || secondString == nil) {
+ return firstString == nil && secondString == nil;
+ }
+
+ return [firstString isEqualToString:secondString];
+}
+
+ (instancetype)stringWithDuration:(vlc_tick_t)duration
currentTime:(vlc_tick_t)time
negative:(BOOL)negative
@@ -115,7 +125,7 @@ NSString * flagEmojiStringForCountryCode(NSString *countryCode)
+ (instancetype)stringWithTime:(long long int)time
{
if (time > 0) {
- if (time > 3600)
+ if (time >= 3600)
return [NSString stringWithFormat:@"%s%01ld:%02ld:%02ld",
time < 0 ? "-" : "",
(long) (time / 3600),
=====================================
modules/gui/macosx/library/VLCInputItem.h
=====================================
@@ -95,17 +95,4 @@ extern NSString * const VLCInputItemCommonDataDifferingFlagString;
NSDictionary<NSString *, id> *commonInputItemData(NSArray<VLCInputItem*> * const inputItems);
- at interface VLCInputNode : NSObject
-
-- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode;
-
- at property (readonly) struct input_item_node_t *vlcInputItemNode;
- at property (readonly, nullable) VLCInputItem *inputItem;
- at property (readonly) int numberOfChildren;
- at property (readonly, nullable) NSArray <VLCInputNode *> *children;
-
-- (void)clearChildrenCache;
-
- at end
-
NS_ASSUME_NONNULL_END
=====================================
modules/gui/macosx/library/VLCInputItem.m
=====================================
@@ -596,7 +596,8 @@ BOOL differing_##prop = NO;
NSString * const firstItem_##prop = firstInputItem.prop;
#define UPDATE_IF_DIFFERING_BOOL(prop) \
-differing_##prop = differing_##prop || ![inputItem.prop isEqualToString:firstItem_##prop];
+differing_##prop = differing_##prop || ![NSString equalOrBothNil:inputItem.prop \
+ to:firstItem_##prop];
#define ADD_PROP_TO_DICT(prop) \
NSString * firstItemValue_##prop = firstItem_##prop == nil ? @"" : firstItem_##prop; \
@@ -615,7 +616,8 @@ NSString * const value_##prop =
PERFORM_ACTION_PER_INPUTITEM_NSSTRING_PROP(UPDATE_IF_DIFFERING_BOOL);
NSString * const inputItem_artworkURL = inputItem.artworkURL.absoluteString;
- differing_artworkURL |= ![inputItem_artworkURL isEqualToString:firstItem_artworkURL];
+ differing_artworkURL |= ![NSString equalOrBothNil:inputItem_artworkURL
+ to:firstItem_artworkURL];
}
PERFORM_ACTION_PER_INPUTITEM_PROP(ADD_PROP_TO_DICT);
@@ -628,73 +630,3 @@ NSString * const value_##prop =
return [commonData copy];
}
-
-
- at interface VLCInputNode ()
-{
- NSArray<VLCInputNode *> *_cachedChildren;
-}
- at end
-
- at implementation VLCInputNode
-
-- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode
-{
- self = [super init];
- if (self && p_inputNode != NULL) {
- _vlcInputItemNode = p_inputNode;
-
- if (_vlcInputItemNode->p_item) {
- _inputItem = [[VLCInputItem alloc] initWithInputItem:_vlcInputItemNode->p_item];
- }
- }
- return self;
-}
-
-- (NSString *)description
-{
- NSString *inputItemName;
- if (_vlcInputItemNode && _vlcInputItemNode->p_item)
- inputItemName = toNSStr(_vlcInputItemNode->p_item->psz_name);
- else
- inputItemName = @"p_item == nil";
- return [NSString stringWithFormat:@"%@: node: %p input name: %@, number of children: %i", NSStringFromClass([self class]),_vlcInputItemNode, inputItemName, self.numberOfChildren];
-}
-
-- (void)clearChildrenCache
-{
- _cachedChildren = nil;
-}
-
-- (int)numberOfChildren
-{
- if (_cachedChildren) {
- return (int)_cachedChildren.count;
- }
- return _vlcInputItemNode ? _vlcInputItemNode->i_children : 0;
-}
-
-- (nullable NSArray<VLCInputNode *> *)children
-{
- if (_cachedChildren) {
- return _cachedChildren;
- }
-
- if (_vlcInputItemNode == NULL) {
- return nil;
- }
- NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:_vlcInputItemNode->i_children];
- for (int i = 0; i < _vlcInputItemNode->i_children; i++) {
- if (_vlcInputItemNode->pp_children == NULL) {
- break;
- }
- VLCInputNode *inputNode = [[VLCInputNode alloc] initWithInputNode:_vlcInputItemNode->pp_children[i]];
- if (inputNode) {
- [mutableArray addObject:inputNode];
- }
- }
- _cachedChildren = [mutableArray copy];
- return _cachedChildren;
-}
-
- at end
=====================================
modules/gui/macosx/library/VLCInputNode.h
=====================================
@@ -0,0 +1,37 @@
+/****************************************************************************
+ * VLCInputNode.h: MacOS X interface module
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
+ * 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>
+
+#import <vlc_common.h>
+#import <vlc_input_item.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+ at class VLCInputItem;
+
+ at interface VLCInputNode : NSObject
+
+- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode;
+
+ at property (readonly) struct input_item_node_t *vlcInputItemNode;
+ at property (readonly, nullable) VLCInputItem *inputItem;
+ at property (readonly) int numberOfChildren;
+ at property (readonly, nullable) NSArray <VLCInputNode *> *children;
+
+- (void)clearChildrenCache;
+
+ at end
+
+NS_ASSUME_NONNULL_END
=====================================
modules/gui/macosx/library/VLCInputNode.m
=====================================
@@ -0,0 +1,88 @@
+/****************************************************************************
+ * VLCInputNode.m: MacOS X interface module
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
+ * 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 "VLCInputNode.h"
+
+#import "VLCInputItem.h"
+
+#import "extensions/NSString+Helpers.h"
+
+ at interface VLCInputNode ()
+{
+ NSArray<VLCInputNode *> *_cachedChildren;
+}
+ at end
+
+ at implementation VLCInputNode
+
+- (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode
+{
+ self = [super init];
+ if (self && p_inputNode != NULL) {
+ _vlcInputItemNode = p_inputNode;
+
+ if (_vlcInputItemNode->p_item) {
+ _inputItem = [[VLCInputItem alloc] initWithInputItem:_vlcInputItemNode->p_item];
+ }
+ }
+ return self;
+}
+
+- (NSString *)description
+{
+ NSString *inputItemName;
+ if (_vlcInputItemNode && _vlcInputItemNode->p_item)
+ inputItemName = toNSStr(_vlcInputItemNode->p_item->psz_name);
+ else
+ inputItemName = @"p_item == nil";
+ return [NSString stringWithFormat:@"%@: node: %p input name: %@, number of children: %i", NSStringFromClass([self class]),_vlcInputItemNode, inputItemName, self.numberOfChildren];
+}
+
+- (void)clearChildrenCache
+{
+ _cachedChildren = nil;
+}
+
+- (int)numberOfChildren
+{
+ if (_cachedChildren) {
+ return (int)_cachedChildren.count;
+ }
+ return _vlcInputItemNode ? _vlcInputItemNode->i_children : 0;
+}
+
+- (nullable NSArray<VLCInputNode *> *)children
+{
+ if (_cachedChildren) {
+ return _cachedChildren;
+ }
+
+ if (_vlcInputItemNode == NULL) {
+ return nil;
+ }
+ NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:_vlcInputItemNode->i_children];
+ for (int i = 0; i < _vlcInputItemNode->i_children; i++) {
+ if (_vlcInputItemNode->pp_children == NULL) {
+ break;
+ }
+ VLCInputNode *inputNode = [[VLCInputNode alloc] initWithInputNode:_vlcInputItemNode->pp_children[i]];
+ if (inputNode) {
+ [mutableArray addObject:inputNode];
+ }
+ }
+ _cachedChildren = [mutableArray copy];
+ return _cachedChildren;
+}
+
+ at end
=====================================
modules/gui/macosx/library/VLCInputNodePathControlItem.m
=====================================
@@ -23,6 +23,7 @@
#import "VLCInputNodePathControlItem.h"
#import "VLCInputItem.h"
+#import "VLCInputNode.h"
#import "VLCLibraryImageCache.h"
#import "extensions/NSString+Helpers.h"
=====================================
modules/gui/macosx/library/VLCLibrarySegment.m
=====================================
@@ -28,6 +28,7 @@
#import "extensions/NSString+Helpers.h"
#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
#import "library/VLCLibraryController.h"
#import "library/VLCLibraryDataTypes.h"
#import "library/VLCLibraryModel.h"
=====================================
modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationStack.m
=====================================
@@ -25,6 +25,7 @@
#import "extensions/NSString+Helpers.h"
#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
#import "library/VLCInputNodePathControl.h"
#import "library/VLCInputNodePathControlItem.h"
#import "library/VLCLibraryWindow.h"
=====================================
modules/gui/macosx/library/media-source/VLCLocalMediaSource.m
=====================================
@@ -23,6 +23,7 @@
#import "VLCLocalMediaSource.h"
#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
#import "VLCLocalMediaSourceNodeObservation.h"
=====================================
modules/gui/macosx/library/media-source/VLCMediaSource.m
=====================================
@@ -25,6 +25,7 @@
#import "extensions/NSString+Helpers.h"
#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
@interface VLCMediaSource ()
{
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
=====================================
@@ -36,6 +36,7 @@
#import "extensions/NSWindow+VLCAdditions.h"
#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
#import "library/VLCInputNodePathControl.h"
#import "library/VLCInputNodePathControlItem.h"
#import "library/VLCLibraryCollectionViewSupplementaryElementView.h"
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
=====================================
@@ -31,6 +31,7 @@
#import "extensions/NSTableCellView+VLCAdditions.h"
#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
#import "library/VLCInputNodePathControl.h"
#import "library/VLCInputNodePathControlItem.h"
#import "library/VLCLibraryImageCache.h"
=====================================
modules/gui/macosx/tests/Makefile.am
=====================================
@@ -9,8 +9,23 @@ check_PROGRAMS += VLCMacOSXTests
TESTS += $(macosx_gui_xctest_bundles)
VLCMacOSXTests_SOURCES = \
+ gui/macosx/tests/VLCHexNumberFormatterTest.m \
+ gui/macosx/tests/NSStringHelpersTest.m \
+ gui/macosx/tests/VLCInputItemTest.m \
+ gui/macosx/tests/VLCInputItemTestSupport.m \
+ gui/macosx/tests/VLCInputNodeTest.m \
+ gui/macosx/tests/VLCLogMessageTest.m \
gui/macosx/tests/VLCPositionFormatterTest.m \
- gui/macosx/extensions/VLCPositionFormatter.m
+ gui/macosx/tests/VLCTimeFormatterTest.m \
+ gui/macosx/extensions/VLCHexNumberFormatter.m \
+ gui/macosx/extensions/VLCPositionFormatter.m \
+ gui/macosx/extensions/NSString+Helpers.m \
+ gui/macosx/library/VLCInputItem.m \
+ gui/macosx/library/VLCInputNode.m \
+ gui/macosx/windows/logging/VLCLogMessage.m \
+ gui/macosx/views/VLCTimeFormatter.m
+
+noinst_HEADERS += gui/macosx/tests/VLCInputItemTestSupport.h
VLCMacOSXTests_CPPFLAGS = \
$(AM_CPPFLAGS) \
@@ -28,10 +43,18 @@ VLCMacOSXTests_LDFLAGS = \
-bundle \
-F"$(macosx_gui_xctest_frameworks_dir)" \
-Wl,-rpath,"$(macosx_gui_xctest_frameworks_dir)" \
+ -Wl,-rpath,"$(abs_top_builddir)/src/.libs" \
-Wl,-framework,XCTest \
- -Wl,-framework,Foundation
+ -Wl,-framework,Foundation \
+ -Wl,-framework,Cocoa \
+ -Wl,-framework,IOKit
+
+VLCMacOSXTests_LDADD = $(top_builddir)/src/libvlccore.la
-VLCMacOSXTests_LDADD =
+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
$(AM_V_GEN)rm -rf $@
=====================================
modules/gui/macosx/tests/NSStringHelpersTest.m
=====================================
@@ -0,0 +1,352 @@
+/****************************************************************************
+ * NSStringHelpersTest.m: NSString helper native 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 of the License, or (at your
+ * option) any later version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+#import <vlc_tick.h>
+
+#import <string.h>
+
+#import "extensions/NSString+Helpers.h"
+#import <vlc_actions.h>
+
+ at interface NSStringHelpersTest : XCTestCase
+ at end
+
+ at implementation NSStringHelpersTest
+
+- (void)testNilSafeStringComparison
+{
+ XCTAssertTrue([NSString equalOrBothNil:nil to:nil]);
+ XCTAssertFalse([NSString equalOrBothNil:nil to:@"value"]);
+ XCTAssertFalse([NSString equalOrBothNil:@"value" to:nil]);
+ XCTAssertTrue([NSString equalOrBothNil:@"value" to:@"value"]);
+ XCTAssertFalse([NSString equalOrBothNil:@"value" to:@"other"]);
+}
+
+- (void)assertVLCKey:(const char *)key equalsFunctionKey:(unichar)functionKey
+{
+ NSString * const expected = [NSString stringWithFormat:@"%C", functionKey];
+ XCTAssertEqualObjects(VLCKeyToString((char *)key), expected);
+}
+
+- (void)testTimeInSecondsFromStringWithColons
+{
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"1:02:03"], 3723);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"02:03"], 123);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"83"], 83);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"00:00:00"], 0);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"1:"], 60);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@":1"], 1);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@":"], 0);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@" 2 : 03"], 123);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"12seconds"], 12);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@"1:2:3:4"], 1);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:@""], 0);
+ XCTAssertEqual([NSString timeInSecondsFromStringWithColons:nil], 0);
+}
+
+- (void)testTimeFormatting
+{
+ XCTAssertEqualObjects([NSString stringWithTime:0], @"--:--");
+ XCTAssertEqualObjects([NSString stringWithTime:-1], @"--:--");
+ XCTAssertEqualObjects([NSString stringWithTime:1], @"00:01");
+ XCTAssertEqualObjects([NSString stringWithTime:59], @"00:59");
+ XCTAssertEqualObjects([NSString stringWithTime:60], @"01:00");
+ XCTAssertEqualObjects([NSString stringWithTime:3599], @"59:59");
+ XCTAssertEqualObjects([NSString stringWithTime:3600], @"1:00:00");
+ XCTAssertEqualObjects([NSString stringWithTime:86400], @"24:00:00");
+
+ XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_0], @"00:00");
+ XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_FROM_SEC(1)], @"00:01");
+ XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_FROM_SEC(60)], @"01:00");
+ XCTAssertEqualObjects([NSString stringWithTimeFromTicks:VLC_TICK_FROM_SEC(3600)], @"1:00:00");
+ XCTAssertEqualObjects([NSString stringWithTimeFromTicks:-VLC_TICK_FROM_SEC(1)], @"-00:01");
+
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:-1], @"--:--");
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:0], @"--:--");
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:1], @"00:00");
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:999], @"00:00");
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:1000], @"00:01");
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:60000], @"01:00");
+ XCTAssertEqualObjects([NSString stringWithTimeFromMilliseconds:3600000], @"1:00:00");
+}
+
+- (void)testDurationFormatting
+{
+ XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(3600)
+ currentTime:VLC_TICK_FROM_SEC(60)
+ negative:YES], @"-59:00");
+ XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(3600)
+ currentTime:VLC_TICK_FROM_SEC(60)
+ negative:NO], @"01:00");
+ XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(60)
+ currentTime:VLC_TICK_FROM_SEC(60)
+ negative:YES], @"-00:00");
+ XCTAssertEqualObjects([NSString stringWithDuration:VLC_TICK_FROM_SEC(60)
+ currentTime:VLC_TICK_FROM_SEC(90)
+ negative:YES], @"-00:00");
+ XCTAssertEqualObjects([NSString stringWithDuration:0
+ currentTime:VLC_TICK_FROM_SEC(60)
+ negative:YES], @"01:00");
+}
+
+- (void)testExtensionsArrayFromVLCStyleString
+{
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"*.mp4;*.mkv"],
+ (@[ @"mp4", @"mkv" ]));
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"srt;ass"],
+ (@[ @"srt", @"ass" ]));
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:NULL], @[]);
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:""], (@[ @"" ]));
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"mp4;"], (@[ @"mp4", @"" ]));
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:";mp4"], (@[ @"", @"mp4" ]));
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"mp4;;mkv"],
+ (@[ @"mp4", @"", @"mkv" ]));
+ XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"*.tar.gz;*.M4V"],
+ (@[ @"tar.gz", @"M4V" ]));
+
+ const char invalidUTF8[] = { (char)0xff, '\0' };
+ XCTAssertNil([NSString extensionsArrayFromVLCStyleString:invalidUTF8]);
+}
+
+- (void)testStringWithIncrementedTrailingNumber
+{
+ XCTAssertEqualObjects([@"Duplicate" stringWithIncrementedTrailingNumber], @"Duplicate (2)");
+ XCTAssertEqualObjects([@"Duplicate (2)" stringWithIncrementedTrailingNumber], @"Duplicate (3)");
+ XCTAssertEqualObjects([@"Duplicate (09)" stringWithIncrementedTrailingNumber],
+ @"Duplicate (10)");
+ XCTAssertEqualObjects([@"" stringWithIncrementedTrailingNumber], @" (2)");
+ XCTAssertEqualObjects([@"Duplicate(2)" stringWithIncrementedTrailingNumber], @"Duplicate(3)");
+ XCTAssertEqualObjects([@"Duplicate (0)" stringWithIncrementedTrailingNumber], @"Duplicate (1)");
+ XCTAssertEqualObjects([@"Duplicate (00)" stringWithIncrementedTrailingNumber],
+ @"Duplicate (1)");
+ XCTAssertEqualObjects([@"Duplicate (2) (3)" stringWithIncrementedTrailingNumber],
+ @"Duplicate (2) (4)");
+ XCTAssertEqualObjects([@"Duplicate (2) " stringWithIncrementedTrailingNumber],
+ @"Duplicate (2) (2)");
+ XCTAssertEqualObjects([@"Duplicate (x)" stringWithIncrementedTrailingNumber],
+ @"Duplicate (x) (2)");
+}
+
+- (void)testFlagEmojiStringForCountryCode
+{
+ XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"us"), @"🇺🇸");
+ XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"HK"), @"🇭🇰");
+ XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"aA"), @"🇦🇦");
+ XCTAssertEqualObjects(flagEmojiStringForCountryCode(@"ZZ"), @"🇿🇿");
+ XCTAssertNil(flagEmojiStringForCountryCode(@"U1"));
+ XCTAssertNil(flagEmojiStringForCountryCode(@""));
+ XCTAssertNil(flagEmojiStringForCountryCode(@"U"));
+ XCTAssertNil(flagEmojiStringForCountryCode(@"USA"));
+ XCTAssertNil(flagEmojiStringForCountryCode(@"U-") );
+ XCTAssertNil(flagEmojiStringForCountryCode(@"éé"));
+ XCTAssertNil(flagEmojiStringForCountryCode(nil));
+
+ for (unichar first = 'A'; first <= 'Z'; first++) {
+ for (unichar second = 'A'; second <= 'Z'; second++) {
+ NSString * const code = [NSString stringWithFormat:@"%C%C", first, second];
+ NSString * const flag = flagEmojiStringForCountryCode(code);
+ XCTAssertNotNil(flag, @"Expected a flag for %@", code);
+ XCTAssertEqual(flag.length, (NSUInteger)4, @"Unexpected UTF-16 length for %@", code);
+ }
+ }
+}
+
+- (void)testBase64EncodingAndDecoding
+{
+ XCTAssertNil([NSString base64StringWithCString:NULL]);
+ XCTAssertEqualObjects([NSString base64StringWithCString:""], @"");
+ XCTAssertEqualObjects([NSString base64StringWithCString:"VLC"], @"VkxD");
+ XCTAssertEqualObjects([@"VLC" base64EncodedString], @"VkxD");
+ XCTAssertEqualObjects([@"" base64EncodedString], @"");
+ XCTAssertEqualObjects([@"😀" base64EncodedString], @"8J+YgA==");
+
+ XCTAssertEqualObjects([@"" base64DecodedString], @"");
+ XCTAssertEqualObjects([@"VkxD" base64DecodedString], @"VLC");
+ XCTAssertEqualObjects([@"8J+YgA==" base64DecodedString], @"😀");
+ XCTAssertEqualObjects([@"!" base64DecodedString], @"");
+ XCTAssertEqualObjects([@"YQ$=" base64DecodedString], @"a");
+
+ const char * const invalidUTF8Base64 = "/w==";
+ XCTAssertNil([@(invalidUTF8Base64) base64DecodedString]);
+ XCTAssertEqualObjects(B64DecNSStr(@"!"), @"");
+
+ char * const input = strdup("VLC");
+ XCTAssertEqualObjects(B64EncAndFree(input), @"VkxD");
+}
+
+- (void)testStringWrapping
+{
+ XCTAssertEqualObjects([@"" stringWrappedToWidth:40], @"");
+
+ NSString * const shortString = @"Short text";
+ XCTAssertEqualObjects([shortString stringWrappedToWidth:1000], shortString);
+ XCTAssertEqualObjects(shortString, @"Short text");
+
+ NSString * const longString =
+ @"This is a long sentence that should wrap across multiple lines.";
+ NSString * const wrappedString = [longString stringWrappedToWidth:40];
+ XCTAssertTrue([wrappedString containsString:@"\n"]);
+ XCTAssertEqualObjects([wrappedString stringByReplacingOccurrencesOfString:@"\n" withString:@""],
+ longString);
+
+ NSString * const multilineString = @"first\nsecond";
+ XCTAssertTrue([[multilineString stringWrappedToWidth:1000] containsString:@"\n"]);
+}
+
+- (void)testCStringConversion
+{
+ XCTAssertEqualObjects(toNSStr(NULL), @"");
+ XCTAssertEqualObjects(toNSStr("VLC"), @"VLC");
+ XCTAssertEqualObjects(toNSStr("caf\xc3\xa9"), @"café");
+
+ const char invalidUTF8[] = { (char)0xff, '\0' };
+ XCTAssertNil(toNSStr(invalidUTF8));
+}
+
+- (void)testCocoaKeyToVLC
+{
+ XCTAssertEqual(CocoaKeyToVLC(NSUpArrowFunctionKey), (unsigned int)KEY_UP);
+ XCTAssertEqual(CocoaKeyToVLC(NSDownArrowFunctionKey), (unsigned int)KEY_DOWN);
+ XCTAssertEqual(CocoaKeyToVLC(NSLeftArrowFunctionKey), (unsigned int)KEY_LEFT);
+ XCTAssertEqual(CocoaKeyToVLC(NSRightArrowFunctionKey), (unsigned int)KEY_RIGHT);
+ XCTAssertEqual(CocoaKeyToVLC(NSInsertFunctionKey), (unsigned int)KEY_INSERT);
+ XCTAssertEqual(CocoaKeyToVLC(NSHomeFunctionKey), (unsigned int)KEY_HOME);
+ XCTAssertEqual(CocoaKeyToVLC(NSEndFunctionKey), (unsigned int)KEY_END);
+ XCTAssertEqual(CocoaKeyToVLC(NSPageUpFunctionKey), (unsigned int)KEY_PAGEUP);
+ XCTAssertEqual(CocoaKeyToVLC(NSPageDownFunctionKey), (unsigned int)KEY_PAGEDOWN);
+ XCTAssertEqual(CocoaKeyToVLC(NSMenuFunctionKey), (unsigned int)KEY_MENU);
+ XCTAssertEqual(CocoaKeyToVLC(NSTabCharacter), (unsigned int)KEY_TAB);
+ XCTAssertEqual(CocoaKeyToVLC(NSCarriageReturnCharacter), (unsigned int)KEY_ENTER);
+ XCTAssertEqual(CocoaKeyToVLC(NSEnterCharacter), (unsigned int)KEY_ENTER);
+ XCTAssertEqual(CocoaKeyToVLC(NSBackspaceCharacter), (unsigned int)KEY_BACKSPACE);
+ XCTAssertEqual(CocoaKeyToVLC(NSDeleteCharacter), (unsigned int)KEY_DELETE);
+
+ NSArray<NSNumber *> * const functionKeys = @[
+ @(NSF1FunctionKey), @(NSF2FunctionKey), @(NSF3FunctionKey), @(NSF4FunctionKey),
+ @(NSF5FunctionKey), @(NSF6FunctionKey), @(NSF7FunctionKey), @(NSF8FunctionKey),
+ @(NSF9FunctionKey), @(NSF10FunctionKey), @(NSF11FunctionKey), @(NSF12FunctionKey)
+ ];
+ for (NSUInteger index = 0; index < functionKeys.count; index++) {
+ XCTAssertEqual(CocoaKeyToVLC(functionKeys[index].unsignedShortValue),
+ (unsigned int)(KEY_F1 + (index << 16)));
+ }
+
+ XCTAssertEqual(CocoaKeyToVLC('A'), (unsigned int)'A');
+ XCTAssertEqual(CocoaKeyToVLC(0), 0U);
+}
+
+- (void)testOSXStringKeyToString
+{
+ XCTAssertEqualObjects(OSXStringKeyToString(@"a"), @"A");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Command+Shift+P"), @"⌘⇧P");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Command+"), @"⌘+");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Ctrl-"), @"⌃-");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Right"), @"→");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Left"), @"←");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Page Up"), @"⇞");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Page Down"), @"⇟");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Up"), @"↑");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Down"), @"↓");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Enter"), @"↵");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Tab"), @"⇥");
+ XCTAssertEqualObjects(OSXStringKeyToString(@"Delete"), @"⌫");
+ XCTAssertTrue([OSXStringKeyToString(@"") length] > 0);
+}
+
+- (void)testVLCKeyToString
+{
+ XCTAssertEqualObjects(VLCKeyToString(NULL), @"");
+ XCTAssertEqualObjects(VLCKeyToString((char *)""), @"");
+ XCTAssertEqualObjects(VLCKeyToString((char *)"a"), @"a");
+ XCTAssertEqualObjects(VLCKeyToString((char *)"Command+Shift+a"), @"a");
+ XCTAssertEqualObjects(VLCKeyToString((char *)"Command+"), @"+");
+ [self assertVLCKey:"Page Up" equalsFunctionKey:NSPageUpFunctionKey];
+ [self assertVLCKey:"Page Down" equalsFunctionKey:NSPageDownFunctionKey];
+ [self assertVLCKey:"Up" equalsFunctionKey:NSUpArrowFunctionKey];
+ [self assertVLCKey:"Down" equalsFunctionKey:NSDownArrowFunctionKey];
+ [self assertVLCKey:"Right" equalsFunctionKey:NSRightArrowFunctionKey];
+ [self assertVLCKey:"Left" equalsFunctionKey:NSLeftArrowFunctionKey];
+ [self assertVLCKey:"Enter" equalsFunctionKey:NSEnterCharacter];
+ [self assertVLCKey:"Insert" equalsFunctionKey:NSInsertFunctionKey];
+ [self assertVLCKey:"Home" equalsFunctionKey:NSHomeFunctionKey];
+ [self assertVLCKey:"End" equalsFunctionKey:NSEndFunctionKey];
+ [self assertVLCKey:"Menu" equalsFunctionKey:NSMenuFunctionKey];
+ [self assertVLCKey:"Tab" equalsFunctionKey:NSTabCharacter];
+ [self assertVLCKey:"Backspace" equalsFunctionKey:NSBackspaceCharacter];
+ [self assertVLCKey:"Delete" equalsFunctionKey:NSDeleteCharacter];
+ [self assertVLCKey:"F12" equalsFunctionKey:NSF12FunctionKey];
+ [self assertVLCKey:"F1" equalsFunctionKey:NSF1FunctionKey];
+ for (NSUInteger functionNumber = 2; functionNumber <= 11; functionNumber++) {
+ NSString * const key = [NSString stringWithFormat:@"F%lu", functionNumber];
+ NSString * const expected = [NSString stringWithFormat:@"%C",
+ (unichar)(NSF1FunctionKey + functionNumber - 1)];
+ XCTAssertEqualObjects(VLCKeyToString((char *)key.UTF8String), expected);
+ }
+ XCTAssertEqualObjects(VLCKeyToString((char *)"Space"), @" ");
+ XCTAssertEqualObjects(VLCKeyToString((char *)"Escape"), @"Escape");
+}
+
+- (void)testVLCModifiersToCocoa
+{
+ XCTAssertEqual(VLCModifiersToCocoa((char *)""), 0U);
+ XCTAssertEqual(VLCModifiersToCocoa((char *)"Command"),
+ (unsigned int)NSEventModifierFlagCommand);
+ XCTAssertEqual(VLCModifiersToCocoa((char *)"Alt"), (unsigned int)NSEventModifierFlagOption);
+ XCTAssertEqual(VLCModifiersToCocoa((char *)"Shift"), (unsigned int)NSEventModifierFlagShift);
+ XCTAssertEqual(VLCModifiersToCocoa((char *)"Ctrl"), (unsigned int)NSEventModifierFlagControl);
+ XCTAssertEqual(VLCModifiersToCocoa((char *)"CommandAltShiftCtrl"),
+ (unsigned int)(NSEventModifierFlagCommand |
+ NSEventModifierFlagOption |
+ NSEventModifierFlagShift |
+ NSEventModifierFlagControl));
+ XCTAssertEqual(VLCModifiersToCocoa((char *)"Unknown"), 0U);
+}
+
+- (void)testVolumeHelpersForTemporaryDirectories
+{
+ NSString * const uuid = [NSUUID UUID].UUIDString;
+ NSString * const path = [NSTemporaryDirectory() stringByAppendingPathComponent:uuid];
+ NSFileManager * const fileManager = [[NSFileManager alloc] init];
+ XCTAssertTrue([fileManager createDirectoryAtPath:path
+ withIntermediateDirectories:NO
+ attributes:nil
+ error:nil]);
+
+ XCTAssertEqualObjects(getVolumeTypeFromMountPath(path), kVLCMediaVideoTSFolder);
+
+ NSString * const vcdPath = [path stringByAppendingPathComponent:kVLCMediaVCD];
+ XCTAssertTrue([fileManager createDirectoryAtPath:vcdPath
+ withIntermediateDirectories:NO
+ attributes:nil
+ error:nil]);
+ XCTAssertEqualObjects(getVolumeTypeFromMountPath(path), kVLCMediaVCD);
+
+ [fileManager removeItemAtPath:vcdPath error:nil];
+ NSString * const bdmvPath = [path stringByAppendingPathComponent:kVLCMediaBDMVFolder];
+ XCTAssertTrue([fileManager createDirectoryAtPath:bdmvPath
+ withIntermediateDirectories:NO
+ attributes:nil
+ error:nil]);
+ XCTAssertEqualObjects(getVolumeTypeFromMountPath(path), kVLCMediaBDMVFolder);
+
+ NSString * const missingPath = [path stringByAppendingPathComponent:@"missing"];
+ XCTAssertEqualObjects(getVolumeTypeFromMountPath(nil), @"");
+ XCTAssertEqualObjects(getVolumeTypeFromMountPath(missingPath), @"");
+ XCTAssertEqualObjects(getBSDNodeFromMountPath(nil), @"");
+ XCTAssertEqualObjects(getBSDNodeFromMountPath(missingPath), @"");
+
+ [fileManager removeItemAtPath:path error:nil];
+}
+
+ at end
=====================================
modules/gui/macosx/tests/VLCHexNumberFormatterTest.m
=====================================
@@ -0,0 +1,50 @@
+/****************************************************************************
+ * VLCHexNumberFormatterTest.m: VLCHexNumberFormatter native 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 of the License, or (at your
+ * option) any later version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "extensions/VLCHexNumberFormatter.h"
+
+ at interface VLCHexNumberFormatterTest : XCTestCase
+ at end
+
+ at implementation VLCHexNumberFormatterTest
+
+- (void)testStringForObjectValue
+{
+ VLCHexNumberFormatter *formatter = [[VLCHexNumberFormatter alloc] init];
+
+ XCTAssertEqualObjects([formatter stringForObjectValue:@0], @"000000");
+ XCTAssertEqualObjects([formatter stringForObjectValue:@(0x12AB)], @"0012AB");
+ XCTAssertNil([formatter stringForObjectValue:@"12AB"]);
+ XCTAssertNil([formatter stringForObjectValue:nil]);
+}
+
+- (void)testGetObjectValue
+{
+ VLCHexNumberFormatter *formatter = [[VLCHexNumberFormatter alloc] init];
+ id object = nil;
+
+ XCTAssertTrue([formatter getObjectValue:&object
+ forString:@"0012AB"
+ errorDescription:nil]);
+ XCTAssertEqualObjects(object, @(0x12AB));
+
+ object = nil;
+ XCTAssertFalse([formatter getObjectValue:&object
+ forString:@"not-hex"
+ errorDescription:nil]);
+ XCTAssertNil(object);
+}
+
+ at end
=====================================
modules/gui/macosx/tests/VLCInputItemTest.m
=====================================
@@ -0,0 +1,489 @@
+/****************************************************************************
+ * VLCInputItemTest.m: VLC input item native 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 <XCTest/XCTest.h>
+
+#import "library/VLCInputItem.h"
+#import "tests/VLCInputItemTestSupport.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+
+ at interface VLCInputItem (VLCInputItemTestPrivate)
+- (void)parsingEnded:(int)status;
+- (void)subTreeAdded:(input_item_node_t *)p_node;
+ at end
+
+ at interface VLCInputItemTest : XCTestCase
+ at end
+
+ at implementation VLCInputItemTest
+
+- (input_item_t *)newFileInputItem
+{
+ return input_item_NewFile("file:///tmp/VLC%20Test.mp4",
+ "Original Name",
+ VLC_TICK_FROM_SEC(123),
+ ITEM_LOCAL);
+}
+
+- (void)testInitializationMapsCoreProperties
+{
+ input_item_t * const coreItem = [self newFileInputItem];
+ XCTAssertNotEqual(coreItem, NULL);
+
+ input_item_SetTitle(coreItem, "Metadata Title");
+ input_item_SetArtist(coreItem, "Artist");
+ input_item_SetAlbum(coreItem, "Album");
+ input_item_SetTrackNumber(coreItem, "4");
+ input_item_SetGenre(coreItem, "Genre");
+ input_item_SetCopyright(coreItem, "Copyright");
+ input_item_SetPublisher(coreItem, "Publisher");
+ input_item_SetLanguage(coreItem, "en");
+ input_item_SetDate(coreItem, "2026");
+ input_item_SetDescription(coreItem, "Description");
+ input_item_SetEncodedBy(coreItem, "Encoder");
+ input_item_SetTrackID(coreItem, "track-id");
+ input_item_SetTrackTotal(coreItem, "10");
+ input_item_SetDirector(coreItem, "Director");
+ input_item_SetSeason(coreItem, "2");
+ input_item_SetEpisode(coreItem, "3");
+ input_item_SetShowName(coreItem, "Show");
+ input_item_SetActors(coreItem, "Actor");
+ input_item_SetDiscNumber(coreItem, "1");
+ input_item_SetDiscTotal(coreItem, "2");
+ input_item_SetMetaExtra(coreItem, "custom", "value");
+ input_item_AddOption(coreItem, "network-caching=1000", 0);
+ input_item_AddOption(coreItem, "http-referrer=https://example.com", 0);
+ input_item_SetArtworkURL(coreItem, "https://example.com/art.jpg");
+
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertNotNil(inputItem);
+ XCTAssertEqualObjects(inputItem.MRL, @"file:///tmp/VLC%20Test.mp4");
+ XCTAssertEqualObjects(inputItem.decodedMRL, @"file:///tmp/VLC Test.mp4");
+ XCTAssertEqualObjects(inputItem.path, @"/tmp/VLC Test.mp4");
+ XCTAssertEqualObjects(inputItem.name, @"Original Name");
+ XCTAssertEqualObjects(inputItem.title, @"Metadata Title");
+ XCTAssertEqualObjects(inputItem.artist, @"Artist");
+ XCTAssertEqualObjects(inputItem.album, @"Album");
+ XCTAssertEqualObjects(inputItem.trackNumber, @"4");
+ XCTAssertEqualObjects(inputItem.genre, @"Genre");
+ XCTAssertEqualObjects(inputItem.copyright, @"Copyright");
+ XCTAssertEqualObjects(inputItem.publisher, @"Publisher");
+ XCTAssertEqualObjects(inputItem.language, @"en");
+ XCTAssertEqualObjects(inputItem.date, @"2026");
+ XCTAssertEqualObjects(inputItem.contentDescription, @"Description");
+ XCTAssertEqualObjects(inputItem.encodedBy, @"Encoder");
+ XCTAssertEqualObjects(inputItem.trackID, @"track-id");
+ XCTAssertEqualObjects(inputItem.trackTotal, @"10");
+ XCTAssertEqualObjects(inputItem.director, @"Director");
+ XCTAssertEqualObjects(inputItem.season, @"2");
+ XCTAssertEqualObjects(inputItem.episode, @"3");
+ XCTAssertEqualObjects(inputItem.showName, @"Show");
+ XCTAssertEqualObjects(inputItem.actors, @"Actor");
+ XCTAssertEqualObjects(inputItem.discNumber, @"1");
+ XCTAssertEqualObjects(inputItem.totalNumberOfDiscs, @"2");
+ XCTAssertEqual(inputItem.duration, VLC_TICK_FROM_SEC(123));
+ XCTAssertEqual(inputItem.inputType, ITEM_TYPE_FILE);
+ XCTAssertFalse(inputItem.isStream);
+ XCTAssertFalse(inputItem.preparsed);
+ XCTAssertEqualObjects(inputItem.options,
+ (@[ @"network-caching=1000", @"http-referrer=https://example.com" ]));
+ XCTAssertEqualObjects([inputItem extraMetaForKey:@"custom"], @"value");
+ XCTAssertEqualObjects(inputItem.extraMetaNames, (@[ @"custom" ]));
+ XCTAssertEqualObjects(inputItem.artworkURL.absoluteString, @"https://example.com/art.jpg");
+ XCTAssertNil(inputItem.finderTags);
+}
+
+- (void)testMetadataSettersUpdateCoreItem
+{
+ input_item_t * const coreItem = [self newFileInputItem];
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ inputItem.name = @"New Name";
+ inputItem.title = @"New Title";
+ inputItem.artist = @"New Artist";
+ inputItem.album = @"New Album";
+ inputItem.trackNumber = @"8";
+ inputItem.genre = @"New Genre";
+ inputItem.copyright = @"New Copyright";
+ inputItem.publisher = @"New Publisher";
+ inputItem.language = @"fr";
+ inputItem.date = @"2027";
+ inputItem.contentDescription = @"New Description";
+ inputItem.director = @"New Director";
+ inputItem.showName = @"New Show";
+ inputItem.actors = @"New Actor";
+
+ XCTAssertEqualObjects(inputItem.name, @"New Name");
+ XCTAssertEqualObjects(inputItem.title, @"New Title");
+ XCTAssertEqualObjects(inputItem.artist, @"New Artist");
+ XCTAssertEqualObjects(inputItem.album, @"New Album");
+ XCTAssertEqualObjects(inputItem.trackNumber, @"8");
+ XCTAssertEqualObjects(inputItem.genre, @"New Genre");
+ XCTAssertEqualObjects(inputItem.copyright, @"New Copyright");
+ XCTAssertEqualObjects(inputItem.publisher, @"New Publisher");
+ XCTAssertEqualObjects(inputItem.language, @"fr");
+ XCTAssertEqualObjects(inputItem.date, @"2027");
+ XCTAssertEqualObjects(inputItem.contentDescription, @"New Description");
+ XCTAssertEqualObjects(inputItem.director, @"New Director");
+ XCTAssertEqualObjects(inputItem.showName, @"New Show");
+ XCTAssertEqualObjects(inputItem.actors, @"New Actor");
+}
+
+- (void)testDefaultValuesAndTitleFallback
+{
+ input_item_t * const coreItem = [self newFileInputItem];
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertEqualObjects(inputItem.title, @"Original Name");
+ XCTAssertEqualObjects(inputItem.options, @[]);
+ XCTAssertEqualObjects(inputItem.extraMetaNames, @[]);
+ XCTAssertEqualObjects([inputItem extraMetaForKey:@"missing"], @"");
+ XCTAssertNil(inputItem.finderTags);
+}
+
+- (void)testCancelParsingWithoutParser
+{
+ input_item_t * const coreItem = [self newFileInputItem];
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertNoThrow([inputItem cancelParsing]);
+}
+
+- (void)testParsingEndedPostsSuccessAndFailureNotifications
+{
+ input_item_t * const coreItem = [self newFileInputItem];
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ NSMutableArray<NSString *> * const notificationNames = [NSMutableArray array];
+ id const observer = [NSNotificationCenter.defaultCenter
+ addObserverForName:nil
+ object:inputItem
+ queue:nil
+ usingBlock:^(NSNotification * const notification) {
+ [notificationNames addObject:notification.name];
+ }];
+
+ [inputItem parsingEnded:1];
+ [inputItem parsingEnded:0];
+
+ [NSNotificationCenter.defaultCenter removeObserver:observer];
+
+ XCTAssertEqual(notificationNames.count, (NSUInteger)2);
+ XCTAssertTrue([notificationNames[0] isEqualToString:VLCInputItemParsingSucceeded]);
+ XCTAssertTrue([notificationNames[1] isEqualToString:VLCInputItemParsingFailed]);
+}
+
+- (void)testSubtreeReplacementPostsNotifications
+{
+ input_item_t * const coreItem = [self newFileInputItem];
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ input_item_node_t * const firstSubtree = input_item_node_Create(inputItem.vlcInputItem);
+ input_item_node_t * const secondSubtree = input_item_node_Create(inputItem.vlcInputItem);
+ NSMutableArray<NSNotification *> * const notifications = [NSMutableArray array];
+ id const observer = [NSNotificationCenter.defaultCenter
+ addObserverForName:VLCInputItemSubtreeAdded
+ object:inputItem
+ queue:nil
+ usingBlock:^(NSNotification * const notification) {
+ [notifications addObject:notification];
+ }];
+
+ [inputItem subTreeAdded:firstSubtree];
+ XCTAssertEqual(inputItem.subTree, firstSubtree);
+ [inputItem subTreeAdded:secondSubtree];
+ XCTAssertEqual(inputItem.subTree, secondSubtree);
+
+ [NSNotificationCenter.defaultCenter removeObserver:observer];
+
+ XCTAssertEqual(notifications.count, (NSUInteger)2);
+ XCTAssertEqual(notifications[0].object, inputItem);
+ XCTAssertEqual(notifications[1].object, inputItem);
+}
+
+- (void)testURLFactoryAndRadioCountryCode
+{
+ NSURL * const url = [NSURL URLWithString:@"file:///tmp/Example%20Video.mkv"];
+ VLCInputItem * const inputItem = [VLCInputItem inputItemFromURL:url];
+
+ XCTAssertNotNil(inputItem);
+ XCTAssertEqualObjects(inputItem.MRL, @"file:///tmp/Example%20Video.mkv");
+ XCTAssertEqualObjects(inputItem.name, @"Example Video");
+ XCTAssertNil(inputItem.radioCountryCodeForFlagArtwork);
+
+ VLCInputItem * const radioItem =
+ [VLCInputItem inputItemFromURL:[NSURL URLWithString:@"radio://us/station"]];
+ XCTAssertEqualObjects(radioItem.radioCountryCodeForFlagArtwork, @"US");
+
+ VLCInputItem * const invalidRadioItem =
+ [VLCInputItem inputItemFromURL:[NSURL URLWithString:@"radio://u1/station"]];
+ XCTAssertNil(invalidRadioItem.radioCountryCodeForFlagArtwork);
+}
+
+- (void)testStreamAndArtworkState
+{
+ input_item_t * const coreItem = input_item_NewStream("https://example.com/live",
+ "Live stream",
+ INPUT_DURATION_INDEFINITE);
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertNotNil(inputItem);
+ XCTAssertTrue(inputItem.isStream);
+ XCTAssertEqual(inputItem.inputType, ITEM_TYPE_STREAM);
+ XCTAssertEqualObjects(inputItem.decodedMRL, @"https://example.com/live");
+ XCTAssertEqualObjects(inputItem.path, @"");
+ XCTAssertEqualObjects(inputItem.options, @[]);
+ XCTAssertNil(inputItem.finderTags);
+
+ inputItem.artworkURL = [NSURL URLWithString:@"https://example.com/live.jpg"];
+ XCTAssertEqualObjects(inputItem.artworkURL.absoluteString, @"https://example.com/live.jpg");
+
+ [(id)inputItem setArtworkURL:nil];
+ XCTAssertNil(inputItem.artworkURL);
+}
+
+- (void)testStreamThumbnailCompletesWithoutImage
+{
+ input_item_t * const coreItem = input_item_NewStream("https://example.com/live",
+ "Live stream",
+ INPUT_DURATION_INDEFINITE);
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTestExpectation * const completionExpectation =
+ [self expectationWithDescription:@"stream thumbnail completion"];
+ [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+ completionHandler:^(NSImage *image) {
+ XCTAssertNil(image);
+ [completionExpectation fulfill];
+ }];
+ [self waitForExpectations:@[completionExpectation] timeout:1.0];
+}
+
+- (void)testLocalThumbnailCompletes
+{
+ VLCInputItemTestResetAppKitState();
+
+ NSString * const path = [NSTemporaryDirectory()
+ stringByAppendingPathComponent:@"VLCInputItemThumbnailTest.txt"];
+ NSData * const contents = [@"VLC thumbnail test" dataUsingEncoding:NSUTF8StringEncoding];
+ XCTAssertTrue([contents writeToFile:path atomically:YES]);
+
+ NSURL * const url = [NSURL fileURLWithPath:path];
+ input_item_t * const coreItem = input_item_NewFile(url.absoluteString.UTF8String,
+ "Thumbnail Test",
+ INPUT_DURATION_UNSET,
+ ITEM_LOCAL);
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ NSImage * const testImage = [[NSImage alloc] initWithSize:NSMakeSize(1, 1)];
+ VLCInputItemTestSetWorkspaceImage(testImage);
+ XCTestExpectation * const completionExpectation =
+ [self expectationWithDescription:@"workspace thumbnail completion"];
+ [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+ completionHandler:^(NSImage *image) {
+ XCTAssertEqualObjects(image, testImage);
+ [completionExpectation fulfill];
+ }];
+ [self waitForExpectations:@[completionExpectation] timeout:2.0];
+
+ VLCInputItemTestSetWorkspaceImage(nil);
+ VLCInputItemTestSetQuickLookImage(testImage);
+ XCTestExpectation * const quickLookExpectation =
+ [self expectationWithDescription:@"Quick Look thumbnail completion"];
+ [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+ completionHandler:^(NSImage *image) {
+ XCTAssertEqualObjects(image, testImage);
+ [quickLookExpectation fulfill];
+ }];
+ [self waitForExpectations:@[quickLookExpectation] timeout:2.0];
+
+ VLCInputItemTestSetQuickLookImage(nil);
+ XCTestExpectation * const emptyExpectation =
+ [self expectationWithDescription:@"empty thumbnail completion"];
+ [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+ completionHandler:^(NSImage *image) {
+ XCTAssertNil(image);
+ [emptyExpectation fulfill];
+ }];
+ [self waitForExpectations:@[emptyExpectation] timeout:2.0];
+
+ [NSFileManager.defaultManager removeItemAtPath:path error:nil];
+ VLCInputItemTestResetAppKitState();
+}
+
+- (void)testLocalFileOperationsUseTheirApplicationServices
+{
+ VLCInputItemTestResetAppKitState();
+ VLCInputItem * const inputItem =
+ [VLCInputItem inputItemFromURL:[NSURL fileURLWithPath:@"/tmp/VLCInputItemTest.mkv"]];
+
+ XCTAssertNotNil(inputItem);
+ [inputItem moveToTrash];
+ XCTAssertTrue(VLCInputItemTestDidReload());
+
+ [inputItem revealInFinder];
+ XCTAssertTrue(VLCInputItemTestDidReveal());
+
+ VLCInputItemTestResetAppKitState();
+}
+
+- (void)testMissingURIShortCircuitsLocalOperations
+{
+ input_item_t * const coreItem = input_item_New(NULL, "No URI");
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertNotNil(inputItem);
+ XCTAssertEqualObjects(inputItem.path, @"");
+ XCTAssertNil(inputItem.finderTags);
+
+ XCTestExpectation * const completionExpectation =
+ [self expectationWithDescription:@"missing URI thumbnail completion"];
+ [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+ completionHandler:^(NSImage *image) {
+ XCTAssertNil(image);
+ [completionExpectation fulfill];
+ }];
+ [self waitForExpectations:@[completionExpectation] timeout:1.0];
+}
+
+- (void)testMalformedLocalURIProducesNoFinderTags
+{
+ input_item_t * const coreItem = input_item_NewFile("file://[",
+ "Malformed",
+ INPUT_DURATION_UNSET,
+ ITEM_LOCAL);
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertNotNil(inputItem);
+ XCTAssertEqualObjects(inputItem.path, @"");
+ XCTAssertNil(inputItem.finderTags);
+
+ XCTestExpectation * const completionExpectation =
+ [self expectationWithDescription:@"malformed URI thumbnail completion"];
+ [inputItem thumbnailWithSize:NSMakeSize(128, 128)
+ completionHandler:^(NSImage *image) {
+ XCTAssertNil(image);
+ [completionExpectation fulfill];
+ }];
+ [self waitForExpectations:@[completionExpectation] timeout:1.0];
+}
+
+- (void)testWritingMetadataForStreamFailsWithoutTouchingTheFilesystem
+{
+ input_item_t * const coreItem = input_item_NewStream("https://example.com/live",
+ "Live stream",
+ INPUT_DURATION_INDEFINITE);
+ VLCInputItem * const inputItem = [[VLCInputItem alloc] initWithInputItem:coreItem];
+ input_item_Release(coreItem);
+
+ XCTAssertNotEqual([inputItem writeMetadataToFile], VLC_SUCCESS);
+}
+
+- (void)testCommonInputItemData
+{
+ XCTAssertEqualObjects(commonInputItemData(@[]), @{});
+
+ input_item_t * const firstCoreItem = [self newFileInputItem];
+ VLCInputItem * const firstInputItem = [[VLCInputItem alloc] initWithInputItem:firstCoreItem];
+ input_item_Release(firstCoreItem);
+
+ XCTAssertEqualObjects(commonInputItemData(@[firstInputItem]),
+ (@{ @"inputItem": firstInputItem }));
+
+ input_item_t * const secondCoreItem = [self newFileInputItem];
+ VLCInputItem * const secondInputItem = [[VLCInputItem alloc] initWithInputItem:secondCoreItem];
+ input_item_Release(secondCoreItem);
+
+ NSDictionary<NSString *, id> *commonData =
+ commonInputItemData(@[ firstInputItem, secondInputItem ]);
+ XCTAssertEqualObjects(commonData[@"MRL"], firstInputItem.MRL);
+ XCTAssertEqualObjects(commonData[@"title"], firstInputItem.title);
+ XCTAssertEqualObjects(commonData[@"artist"], @"");
+ XCTAssertEqualObjects(commonData[@"artworkURL"], @"");
+
+ secondInputItem.artist = @"Different artist";
+ secondInputItem.artworkURL = [NSURL URLWithString:@"https://example.com/art.jpg"];
+ commonData = commonInputItemData(@[ firstInputItem, secondInputItem ]);
+ XCTAssertEqualObjects(commonData[@"artist"], VLCInputItemCommonDataDifferingFlagString);
+ XCTAssertEqualObjects(commonData[@"artworkURL"], VLCInputItemCommonDataDifferingFlagString);
+}
+
+- (void)testCommonInputItemDataMarksEveryDifferingStringProperty
+{
+ input_item_t * const firstCoreItem = [self newFileInputItem];
+ VLCInputItem * const firstInputItem = [[VLCInputItem alloc] initWithInputItem:firstCoreItem];
+ input_item_Release(firstCoreItem);
+
+ input_item_t * const secondCoreItem = input_item_NewFile("file:///tmp/Other.mp4",
+ "Other Name",
+ VLC_TICK_FROM_SEC(123),
+ ITEM_LOCAL);
+ input_item_SetTitle(secondCoreItem, "Other Title");
+ input_item_SetArtist(secondCoreItem, "Other Artist");
+ input_item_SetAlbum(secondCoreItem, "Other Album");
+ input_item_SetTrackNumber(secondCoreItem, "9");
+ input_item_SetTrackTotal(secondCoreItem, "11");
+ input_item_SetGenre(secondCoreItem, "Other Genre");
+ input_item_SetDate(secondCoreItem, "2027");
+ input_item_SetEpisode(secondCoreItem, "4");
+ input_item_SetActors(secondCoreItem, "Other Actor");
+ input_item_SetDirector(secondCoreItem, "Other Director");
+ input_item_SetShowName(secondCoreItem, "Other Show");
+ input_item_SetCopyright(secondCoreItem, "Other Copyright");
+ input_item_SetPublisher(secondCoreItem, "Other Publisher");
+ input_item_SetNowPlaying(secondCoreItem, "Other Now Playing");
+ input_item_SetLanguage(secondCoreItem, "de");
+ input_item_SetDescription(secondCoreItem, "Other Description");
+ input_item_SetEncodedBy(secondCoreItem, "Other Encoder");
+ input_item_SetArtworkURL(secondCoreItem, "https://example.com/other-art.jpg");
+ input_item_SetTrackID(secondCoreItem, "other-track-id");
+ VLCInputItem * const secondInputItem = [[VLCInputItem alloc] initWithInputItem:secondCoreItem];
+ input_item_Release(secondCoreItem);
+
+ NSDictionary<NSString *, id> * const commonData =
+ commonInputItemData(@[ firstInputItem, secondInputItem, secondInputItem ]);
+ NSArray<NSString *> * const differingProperties = @[
+ @"MRL", @"decodedMRL", @"title", @"artist", @"album", @"trackNumber",
+ @"trackTotal", @"genre", @"date", @"episode", @"actors", @"director",
+ @"showName", @"copyright", @"publisher", @"nowPlaying", @"language",
+ @"contentDescription", @"encodedBy", @"artworkURL"
+ ];
+ for (NSString * const property in differingProperties) {
+ XCTAssertEqualObjects(commonData[property], VLCInputItemCommonDataDifferingFlagString,
+ @"%@ should be marked as differing", property);
+ }
+}
+
+- (void)testRejectsNilInputItem
+{
+ input_item_t * const nilInputItem = NULL;
+ XCTAssertNil([[VLCInputItem alloc] initWithInputItem:nilInputItem]);
+}
+
+ at end
+
+#pragma clang diagnostic pop
=====================================
modules/gui/macosx/tests/VLCInputItemTestSupport.h
=====================================
@@ -0,0 +1,18 @@
+/****************************************************************************
+ * VLCInputItemTestSupport.h: VLC input item test support declarations
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * 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 <Cocoa/Cocoa.h>
+
+void VLCInputItemTestSetQuickLookImage(NSImage * _Nullable image);
+void VLCInputItemTestSetWorkspaceImage(NSImage * _Nullable image);
+void VLCInputItemTestResetAppKitState(void);
+BOOL VLCInputItemTestDidReveal(void);
+BOOL VLCInputItemTestDidReload(void);
=====================================
modules/gui/macosx/tests/VLCInputItemTestSupport.m
=====================================
@@ -0,0 +1,131 @@
+/****************************************************************************
+ * VLCInputItemTestSupport.m: VLC input item test-only application stubs
+ ****************************************************************************
+ * 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 <Cocoa/Cocoa.h>
+#import <vlc_interface.h>
+
+#import "VLCInputItemTestSupport.h"
+
+static NSImage *sQuickLookImage;
+static NSImage *sWorkspaceImage;
+static BOOL sDidReveal;
+static BOOL sDidReload;
+
+ at interface VLCLibraryController : NSObject
+ at end
+
+ at interface VLCMain : NSObject
++ (instancetype)sharedInstance;
+ at end
+
+ at implementation VLCLibraryController
+
+- (void)reloadMediaLibraryFoldersForInputItems:(NSArray * __unused)inputItems
+{
+ sDidReload = YES;
+}
+
+ at end
+
+ at implementation VLCMain
+
++ (instancetype)sharedInstance
+{
+ static VLCMain *main;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ main = [[VLCMain alloc] init];
+ });
+ return main;
+}
+
+- (VLCLibraryController *)libraryController
+{
+ static VLCLibraryController *libraryController;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ libraryController = [[VLCLibraryController alloc] init];
+ });
+ return libraryController;
+}
+
+ at end
+
+intf_thread_t *getIntf(void)
+{
+ return NULL;
+}
+
+void VLCInputItemTestSetQuickLookImage(NSImage *image)
+{
+ sQuickLookImage = image;
+}
+
+void VLCInputItemTestSetWorkspaceImage(NSImage *image)
+{
+ sWorkspaceImage = image;
+}
+
+void VLCInputItemTestResetAppKitState(void)
+{
+ sQuickLookImage = nil;
+ sWorkspaceImage = nil;
+ sDidReveal = NO;
+ sDidReload = NO;
+}
+
+BOOL VLCInputItemTestDidReveal(void)
+{
+ return sDidReveal;
+}
+
+BOOL VLCInputItemTestDidReload(void)
+{
+ return sDidReload;
+}
+
+ at interface NSImage (VLCInputItemTestSupport)
++ (void)quickLookPreviewForLocalPath:(NSString *)path
+ withSize:(NSSize)size
+ completionHandler:(void (^)(NSImage *image))completionHandler;
+ at end
+
+ at interface NSWorkspace (VLCInputItemTestSupport)
+- (NSImage *)iconForFile:(NSString *)fullPath;
+- (void)activateFileViewerSelectingURLs:(NSArray<NSURL *> *)fileURLs;
+ at end
+
+ at implementation NSWorkspace (VLCInputItemTestSupport)
+
+- (NSImage *)iconForFile:(NSString * __unused)fullPath
+{
+ return sWorkspaceImage;
+}
+
+- (void)activateFileViewerSelectingURLs:(NSArray<NSURL *> * __unused)fileURLs
+{
+ sDidReveal = YES;
+}
+
+ at end
+
+ at implementation NSImage (VLCInputItemTestSupport)
+
++ (void)quickLookPreviewForLocalPath:(NSString * __unused)path
+ withSize:(NSSize __unused)size
+ completionHandler:(void (^)(NSImage *image))completionHandler
+{
+ completionHandler(sQuickLookImage);
+}
+
+ at end
=====================================
modules/gui/macosx/tests/VLCInputNodeTest.m
=====================================
@@ -0,0 +1,219 @@
+/****************************************************************************
+ * VLCInputNodeTest.m: VLC input node native 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 <XCTest/XCTest.h>
+
+#import "library/VLCInputItem.h"
+#import "library/VLCInputNode.h"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+
+ at interface VLCInputNodeTest : XCTestCase
+ at end
+
+ at implementation VLCInputNodeTest
+
+- (input_item_node_t *)newRootNodeWithRootItem:(input_item_t **)rootItem
+ childOne:(input_item_t **)childOne
+ childTwo:(input_item_t **)childTwo
+{
+ *rootItem = input_item_NewDirectory("vlc://root", "Root", ITEM_LOCAL);
+ *childOne = input_item_NewFile("file:///tmp/One.mp4", "One", VLC_TICK_FROM_SEC(1), ITEM_LOCAL);
+ *childTwo = input_item_NewFile("file:///tmp/Two.mp4", "Two", VLC_TICK_FROM_SEC(2), ITEM_LOCAL);
+
+ input_item_node_t * const rootNode = input_item_node_Create(*rootItem);
+ input_item_node_AppendItem(rootNode, *childOne);
+ return rootNode;
+}
+
+- (void)testRootAndChildrenMapToInputItems
+{
+ input_item_t *rootItem = NULL;
+ input_item_t *childOne = NULL;
+ input_item_t *childTwo = NULL;
+ input_item_node_t * const rootNode =
+ [self newRootNodeWithRootItem:&rootItem childOne:&childOne childTwo:&childTwo];
+ input_item_node_AppendItem(rootNode, childTwo);
+
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:rootNode];
+
+ XCTAssertEqual(inputNode.vlcInputItemNode, rootNode);
+ XCTAssertNotNil(inputNode.inputItem);
+ XCTAssertEqualObjects(inputNode.inputItem.name, @"Root");
+ XCTAssertEqual(inputNode.numberOfChildren, 2);
+
+ NSArray<VLCInputNode *> * const children = inputNode.children;
+ XCTAssertEqual(children.count, (NSUInteger)2);
+ XCTAssertEqualObjects(children[0].inputItem.name, @"One");
+ XCTAssertEqualObjects(children[1].inputItem.name, @"Two");
+ XCTAssertEqualObjects(children[0].inputItem.MRL, @"file:///tmp/One.mp4");
+ XCTAssertEqualObjects(children[1].inputItem.MRL, @"file:///tmp/Two.mp4");
+ XCTAssertEqual(children[0].vlcInputItemNode, rootNode->pp_children[0]);
+ XCTAssertEqual(children[1].vlcInputItemNode, rootNode->pp_children[1]);
+
+ NSString * const description = inputNode.description;
+ XCTAssertTrue([description containsString:@"Root"]);
+ XCTAssertTrue([description containsString:@"number of children: 2"]);
+
+ input_item_node_Delete(rootNode);
+ input_item_Release(rootItem);
+ input_item_Release(childOne);
+ input_item_Release(childTwo);
+}
+
+- (void)testLeafNodeWithAnInputItemHasNoChildren
+{
+ input_item_t * const item =
+ input_item_NewFile("file:///tmp/Leaf.mp4", "Leaf", VLC_TICK_FROM_SEC(3), ITEM_LOCAL);
+ input_item_node_t * const node = input_item_node_Create(item);
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:node];
+
+ XCTAssertEqual(inputNode.vlcInputItemNode, node);
+ XCTAssertEqualObjects(inputNode.inputItem.name, @"Leaf");
+ XCTAssertEqual(inputNode.numberOfChildren, 0);
+ XCTAssertNotNil(inputNode.children);
+ XCTAssertEqual(inputNode.children.count, (NSUInteger)0);
+ XCTAssertTrue([inputNode.description containsString:@"number of children: 0"]);
+
+ input_item_node_Delete(node);
+ input_item_Release(item);
+}
+
+- (void)testNestedNodesAreWrappedRecursively
+{
+ input_item_t * const rootItem =
+ input_item_NewDirectory("vlc://root", "Root", ITEM_LOCAL);
+ input_item_t * const directoryItem =
+ input_item_NewDirectory("vlc://directory", "Directory", ITEM_LOCAL);
+ input_item_t * const leafItem =
+ input_item_NewFile("file:///tmp/Nested.mp4", "Nested", VLC_TICK_FROM_SEC(4), ITEM_LOCAL);
+
+ input_item_node_t * const rootNode = input_item_node_Create(rootItem);
+ input_item_node_t * const directoryNode =
+ input_item_node_AppendItem(rootNode, directoryItem);
+ input_item_node_t * const leafNode =
+ input_item_node_AppendItem(directoryNode, leafItem);
+
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:rootNode];
+ VLCInputNode * const directoryInputNode = inputNode.children.firstObject;
+ VLCInputNode * const leafInputNode = directoryInputNode.children.firstObject;
+
+ XCTAssertEqual(inputNode.numberOfChildren, 1);
+ XCTAssertEqual(directoryInputNode.vlcInputItemNode, directoryNode);
+ XCTAssertEqualObjects(directoryInputNode.inputItem.name, @"Directory");
+ XCTAssertEqual(directoryInputNode.numberOfChildren, 1);
+ XCTAssertEqual(leafInputNode.vlcInputItemNode, leafNode);
+ XCTAssertEqualObjects(leafInputNode.inputItem.name, @"Nested");
+ XCTAssertEqual(leafInputNode.numberOfChildren, 0);
+ XCTAssertEqualObjects(leafInputNode.children, @[]);
+
+ input_item_node_Delete(rootNode);
+ input_item_Release(rootItem);
+ input_item_Release(directoryItem);
+ input_item_Release(leafItem);
+}
+
+- (void)testChildrenAreCachedUntilCleared
+{
+ input_item_t *rootItem = NULL;
+ input_item_t *childOne = NULL;
+ input_item_t *childTwo = NULL;
+ input_item_node_t * const rootNode =
+ [self newRootNodeWithRootItem:&rootItem childOne:&childOne childTwo:&childTwo];
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:rootNode];
+
+ NSArray<VLCInputNode *> * const initialChildren = inputNode.children;
+ XCTAssertEqual(initialChildren.count, (NSUInteger)1);
+ XCTAssertEqual(inputNode.numberOfChildren, 1);
+
+ input_item_node_AppendItem(rootNode, childTwo);
+ XCTAssertEqual(inputNode.numberOfChildren, 1);
+ XCTAssertEqual(inputNode.children, initialChildren);
+
+ [inputNode clearChildrenCache];
+ XCTAssertEqual(inputNode.numberOfChildren, 2);
+ NSArray<VLCInputNode *> * const refreshedChildren = inputNode.children;
+ XCTAssertNotEqual(refreshedChildren, initialChildren);
+ XCTAssertEqual(refreshedChildren.count, (NSUInteger)2);
+ XCTAssertEqualObjects(refreshedChildren[1].inputItem.name, @"Two");
+
+ input_item_node_Delete(rootNode);
+ input_item_Release(rootItem);
+ input_item_Release(childOne);
+ input_item_Release(childTwo);
+}
+
+- (void)testNodeWithoutAnInputItem
+{
+ input_item_node_t node = { 0 };
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:&node];
+
+ XCTAssertNotNil(inputNode);
+ XCTAssertEqual(inputNode.vlcInputItemNode, &node);
+ XCTAssertNil(inputNode.inputItem);
+ XCTAssertEqual(inputNode.numberOfChildren, 0);
+ XCTAssertEqualObjects(inputNode.children, @[]);
+ XCTAssertTrue([inputNode.description containsString:@"p_item == nil"]);
+}
+
+- (void)testNilNodeHasEmptyState
+{
+ input_item_node_t * const nilNode = NULL;
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:nilNode];
+
+ XCTAssertNotNil(inputNode);
+ XCTAssertTrue(inputNode.vlcInputItemNode == NULL);
+ XCTAssertNil(inputNode.inputItem);
+ XCTAssertEqual(inputNode.numberOfChildren, 0);
+ XCTAssertNil(inputNode.children);
+ [inputNode clearChildrenCache];
+ XCTAssertNil(inputNode.children);
+ XCTAssertTrue([inputNode.description containsString:@"p_item == nil"]);
+}
+
+- (void)testMalformedNodeWithMissingChildrenArrayDoesNotCrash
+{
+ input_item_node_t node = {
+ .p_item = NULL,
+ .i_children = 1,
+ .pp_children = NULL,
+ };
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:&node];
+
+ XCTAssertEqual(inputNode.numberOfChildren, 1);
+ XCTAssertEqualObjects(inputNode.children, @[]);
+ XCTAssertEqual(inputNode.numberOfChildren, 0);
+}
+
+- (void)testMalformedNodeWithNullChildSlotIsWrappedSafely
+{
+ input_item_node_t *nullChild = NULL;
+ input_item_node_t node = {
+ .p_item = NULL,
+ .i_children = 1,
+ .pp_children = &nullChild,
+ };
+ VLCInputNode * const inputNode = [[VLCInputNode alloc] initWithInputNode:&node];
+
+ NSArray<VLCInputNode *> * const children = inputNode.children;
+ XCTAssertEqual(inputNode.numberOfChildren, 1);
+ XCTAssertEqual(children.count, (NSUInteger)1);
+ XCTAssertTrue(children.firstObject.vlcInputItemNode == NULL);
+ XCTAssertNil(children.firstObject.inputItem);
+ XCTAssertNil(children.firstObject.children);
+}
+
+ at end
+
+#pragma clang diagnostic pop
=====================================
modules/gui/macosx/tests/VLCLogMessageTest.m
=====================================
@@ -0,0 +1,124 @@
+/****************************************************************************
+ * VLCLogMessageTest.m: VLC log message native 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 <XCTest/XCTest.h>
+
+#import "windows/logging/VLCLogMessage.h"
+#import <vlc_messages.h>
+
+ at interface VLCLogMessageTest : XCTestCase
+ at end
+
+ at implementation VLCLogMessageTest
+
+- (vlc_log_t)logInfo
+{
+ vlc_log_t info = { 0 };
+ info.psz_module = "core";
+ info.file = "input.c";
+ info.line = 42;
+ info.func = "OpenInput";
+ return info;
+}
+
+- (void)testMessageProperties
+{
+ vlc_log_t info = [self logInfo];
+ char message[] = "Could not open media";
+
+ VLCLogMessage * const logMessage =
+ [[VLCLogMessage alloc] initWithMessage:message
+ type:VLC_MSG_ERR
+ info:&info];
+
+ XCTAssertNotNil(logMessage);
+ XCTAssertEqual(logMessage.type, VLC_MSG_ERR);
+ XCTAssertEqualObjects(logMessage.message, @"Could not open media");
+ XCTAssertEqualObjects(logMessage.component, @"core");
+ XCTAssertEqualObjects(logMessage.function, @"OpenInput");
+ XCTAssertEqualObjects(logMessage.location, @"input.c:42");
+ XCTAssertEqualObjects(logMessage.typeName, @"error");
+ XCTAssertEqualObjects(logMessage.fullMessage, @"core error: Could not open media");
+}
+
+- (void)testFactoryMethod
+{
+ vlc_log_t info = [self logInfo];
+ char message[] = "Started";
+
+ VLCLogMessage * const logMessage = [VLCLogMessage logMessage:message
+ type:VLC_MSG_INFO
+ info:&info];
+
+ XCTAssertNotNil(logMessage);
+ XCTAssertEqualObjects(logMessage.fullMessage, @"core info: Started");
+}
+
+- (void)testTypeNames
+{
+ const struct {
+ int type;
+ NSString *name;
+ } cases[] = {
+ { VLC_MSG_INFO, @"info" },
+ { VLC_MSG_ERR, @"error" },
+ { VLC_MSG_WARN, @"warning" },
+ { VLC_MSG_DBG, @"debug" },
+ { 99, @"unknown" },
+ };
+
+ for (NSUInteger index = 0; index < sizeof(cases) / sizeof(cases[0]); index++) {
+ vlc_log_t info = [self logInfo];
+ char message[] = "Message";
+ VLCLogMessage * const logMessage =
+ [[VLCLogMessage alloc] initWithMessage:message
+ type:cases[index].type
+ info:&info];
+
+ XCTAssertEqualObjects(logMessage.typeName, cases[index].name);
+ }
+}
+
+- (void)testEmptyMessage
+{
+ vlc_log_t info = [self logInfo];
+ char message[] = "";
+
+ VLCLogMessage * const logMessage =
+ [[VLCLogMessage alloc] initWithMessage:message
+ type:VLC_MSG_INFO
+ info:&info];
+
+ XCTAssertNotNil(logMessage);
+ XCTAssertEqualObjects(logMessage.message, @"");
+ XCTAssertEqualObjects(logMessage.fullMessage, @"core info: ");
+}
+
+- (void)testRejectsMissingMessageOrLogInfo
+{
+ vlc_log_t info = [self logInfo];
+
+ XCTAssertNil([[VLCLogMessage alloc] initWithMessage:NULL
+ type:VLC_MSG_INFO
+ info:&info]);
+ XCTAssertNil([[VLCLogMessage alloc] initWithMessage:(char *)"Message"
+ type:VLC_MSG_INFO
+ info:NULL]);
+
+ info.psz_module = NULL;
+ XCTAssertNil([[VLCLogMessage alloc] initWithMessage:(char *)"Message"
+ type:VLC_MSG_INFO
+ info:&info]);
+}
+
+ at end
=====================================
modules/gui/macosx/tests/VLCTimeFormatterTest.m
=====================================
@@ -0,0 +1,88 @@
+/****************************************************************************
+ * VLCTimeFormatterTest.m: VLCTimeFormatter native 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 of the License, or (at your
+ * option) any later version.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "views/VLCTimeFormatter.h"
+
+ at interface VLCTimeFormatterTest : XCTestCase
+ at end
+
+ at implementation VLCTimeFormatterTest
+
+- (void)testStringForObjectValue
+{
+ VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+
+ XCTAssertEqualObjects([formatter stringForObjectValue:@83], @"01:23");
+ XCTAssertEqualObjects([formatter stringForObjectValue:@3601], @"1:00:01");
+ XCTAssertEqualObjects([formatter stringForObjectValue:@0], @"--:--");
+}
+
+- (void)testGetObjectValueFromMinutesAndSeconds
+{
+ VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+ id object = nil;
+ NSString *error = nil;
+
+ XCTAssertTrue([formatter getObjectValue:&object
+ forString:@"01:23"
+ errorDescription:&error]);
+ XCTAssertEqualObjects(object, @83000);
+ XCTAssertNil(error);
+}
+
+- (void)testGetObjectValueFromSeconds
+{
+ VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+ id object = nil;
+
+ XCTAssertTrue([formatter getObjectValue:&object
+ forString:@"83"
+ errorDescription:nil]);
+ XCTAssertEqualObjects(object, @83000);
+}
+
+- (void)testGetObjectValueFromHoursMinutesAndSeconds
+{
+ VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+ id object = nil;
+
+ XCTAssertTrue([formatter getObjectValue:&object
+ forString:@"1:02:03"
+ errorDescription:nil]);
+ XCTAssertEqualObjects(object, @3723000);
+}
+
+- (void)testGetObjectValueRejectsInvalidComponentCount
+{
+ VLCTimeFormatter * const formatter = [VLCTimeFormatter new];
+ id object = nil;
+ NSString *error = nil;
+
+ XCTAssertFalse([formatter getObjectValue:&object
+ forString:@"1:2:3:4"
+ errorDescription:&error]);
+ XCTAssertNil(object);
+ XCTAssertNotNil(error);
+
+ object = nil;
+ error = nil;
+ XCTAssertFalse([formatter getObjectValue:&object
+ forString:@""
+ errorDescription:&error]);
+ XCTAssertNil(object);
+ XCTAssertNotNil(error);
+}
+
+ at end
=====================================
modules/gui/macosx/views/VLCTimeFormatter.m
=====================================
@@ -39,22 +39,22 @@
{
NSArray<NSString *> * const components = [string componentsSeparatedByString:@":"];
const NSUInteger componentCount = components.count;
- if (componentCount <= 1 || componentCount > 3) {
+ if (string.length == 0 || componentCount > 3) {
*error = @"Cannot get bookmark time as invalid string format for time was received";
return NO;
}
- size_t time = 0;
+ int64_t time = 0;
if (componentCount == 1) {
time = components.firstObject.longLongValue * 1000;
} else if (componentCount == 2) {
- time = components.firstObject.longLongValue * 60 +
- [components objectAtIndex:1].longLongValue * 1000;
+ time = (components.firstObject.longLongValue * 60 +
+ [components objectAtIndex:1].longLongValue) * 1000;
} else if (componentCount == 3) {
- time = components.firstObject.longLongValue * 3600 +
- [components objectAtIndex:1].longLongValue * 60 +
- [components objectAtIndex:2].longLongValue * 1000;
+ time = (components.firstObject.longLongValue * 3600 +
+ [components objectAtIndex:1].longLongValue * 60 +
+ [components objectAtIndex:2].longLongValue) * 1000;
}
*obj = [NSNumber numberWithLongLong:time];
=====================================
modules/gui/macosx/windows/logging/VLCLogMessage.m
=====================================
@@ -33,7 +33,7 @@
{
self = [super init];
if (self) {
- if (!message || !info->psz_module)
+ if (!message || !info || !info->psz_module)
return nil;
_type = type;
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/348464878bd258293b464098ecc997adef57e016...44c4002ead30fca927627d45a4a5fe6306f3e461
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/348464878bd258293b464098ecc997adef57e016...44c4002ead30fca927627d45a4a5fe6306f3e461
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