2026-10-17 19:12:43 +02:00
using System.Text.Json ;
using AIStudio.Settings ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools ;
using AIStudio.Tools.Rust ;
using AIStudio.Tools.Validation ;
namespace AIStudio.Tests.Tools ;
public sealed class LocalDirectoryFilePatternsTests
{
[Test]
public void MissingDirectorySelectionSettingsUseTheirDefaults ()
{
var dataSource = JsonSerializer . Deserialize < DataSourceLocalDirectory >( "{}" , SettingsManager . JSON_OPTIONS );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( dataSource . FilePatterns , Is . EqualTo ( FileNamePatterns . ALL_DOCUMENTS_PATTERN ), "A data source from before file patterns has to keep indexing what it indexed." );
Assert . That ( dataSource . IncludeSubdirectories , Is . True );
Assert . That ( dataSource . IncludeHiddenFilesAndFolders , Is . False );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
[Test]
public void ExistingBatchSettingsDefaultToSkippingHiddenFilesAndFolders ()
{
var settings = JsonSerializer . Deserialize < DataBatchProcessing >( "{}" , SettingsManager . JSON_OPTIONS );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( settings , Is . Not . Null );
Assert . That ( settings !. IncludeSubdirectories , Is . EqualTo ( DataBatchProcessing . DEFAULT_INCLUDE_SUBDIRECTORIES ));
Assert . That ( settings . IncludeHiddenFilesAndFolders , Is . EqualTo ( DataBatchProcessing . DEFAULT_INCLUDE_HIDDEN_FILES_AND_FOLDERS ));
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
2026-10-27 19:57:04 +01:00
[TestCase(";")]
[TestCase(" ; ; ")]
2026-10-17 19:12:43 +02:00
[TestCase("folder/*.txt")]
public void InvalidStoredPatternsFallBackWithoutRemovingExistingDocuments ( string? storedPatterns )
{
var dataSource = new DataSourceLocalDirectory { FilePatterns = storedPatterns ! };
Assert . That ( dataSource . EffectiveFilePatterns , Is . EqualTo ( DataSourceLocalDirectory . DEFAULT_FILE_PATTERNS ));
}
2026-10-27 19:57:04 +01:00
[Test]
public void BatchExclusionsSurviveSettingsSerialization ()
{
var written = new DataBatchProcessing { ExcludePatterns = "*.mp3;*.webm;draft-*" };
var json = JsonSerializer . Serialize ( written , SettingsManager . JSON_OPTIONS );
var read = JsonSerializer . Deserialize < DataBatchProcessing >( json , SettingsManager . JSON_OPTIONS );
Assert . That ( read !. ExcludePatterns , Is . EqualTo ( written . ExcludePatterns ));
}
2026-10-17 19:12:43 +02:00
[Test]
public void PatternsAreSplitAndTrimmed ()
{
Assert . That ( FileNamePatterns . Parse ( " *.txt ; *.tf ; .gitignore " ), Is . EqualTo ( new [] { "*.txt" , "*.tf" , ".gitignore" }));
}
2026-10-27 19:57:04 +01:00
[TestCase("*.txt;")]
[TestCase(";*.txt")]
[TestCase(";; *.txt ; ; ")]
public void EmptyEntriesAreIgnoredWithoutBroadeningTheSelection ( string patterns )
{
var parsed = FileNamePatterns . Parse ( patterns );
var dataSource = new DataSourceLocalDirectory { FilePatterns = patterns };
var selection = new FolderFileSelection ( parsed , [], false , false , FolderFileSelection . DEFAULT_MAX_FILE_SIZE_MEGABYTES );
var canonical = new FolderFileSelection ([ "*.txt" ], [], false , false , FolderFileSelection . DEFAULT_MAX_FILE_SIZE_MEGABYTES );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-27 19:57:04 +01:00
{
Assert . That ( FileNamePatterns . IsValid ( patterns ), Is . True );
Assert . That ( FileNamePatterns . Validate ( patterns ), Is . Null );
Assert . That ( parsed , Is . EqualTo ( new [] { "*.txt" }));
Assert . That ( dataSource . EffectiveFilePatterns , Is . EqualTo ( patterns ), "Empty entries must not trigger the default document selection." );
Assert . That ( selection . Selects ( "report.txt" ), Is . True );
Assert . That ( selection . Selects ( "report.md" ), Is . False );
Assert . That ( selection . Selects ( "recording.wav" ), Is . False );
Assert . That ( selection . Describe (), Is . EqualTo ( canonical . Describe ()), "Empty entries must not change the indexing selection signature." );
2026-11-01 20:32:16 +01:00
}
2026-10-27 19:57:04 +01:00
}
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
[TestCase(";")]
[TestCase(" ; ;; ")]
public void IncludesStillRequireAtLeastOneNonEmptyPattern ( string? patterns )
{
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-27 19:57:04 +01:00
{
Assert . That ( FileNamePatterns . Parse ( patterns ), Is . Empty );
Assert . That ( FileNamePatterns . IsValid ( patterns ), Is . False );
Assert . That ( FileNamePatterns . Validate ( patterns ), Is . Not . Null );
2026-11-01 20:32:16 +01:00
}
2026-10-27 19:57:04 +01:00
}
2026-10-17 19:12:43 +02:00
[Test]
public void NormalizingPatternsRemovesCaseInsensitiveDuplicates ()
{
Assert . That ( FileNamePatterns . Normalize ( " *.txt ; *.TF ; *.tf ; :text: ; :TEXT: " ), Is . EqualTo ( "*.txt;*.TF;:text:" ));
}
[Test]
public void PatternSetsAreEquivalentRegardlessOfOrderOrDuplicates ()
{
Assert . That (
FileNamePatterns . AreEquivalent ([ "*.txt" , "*.tf" , "*.TF" ], [ "*.TF" , "*.TXT" ]),
Is . True );
}
[Test]
public void FilePatternsSurvivePolymorphicSettingsSerialization ()
{
IDataSource written = new DataSourceLocalDirectory
{
Type = DataSourceType . LOCAL_DIRECTORY ,
FilePatterns = "*.txt;*.tf;.gitignore" ,
2026-10-27 19:57:04 +01:00
ExcludePatterns = "secret-*;*.backup" ,
2026-10-17 19:12:43 +02:00
IncludeSubdirectories = false ,
IncludeHiddenFilesAndFolders = true ,
};
var json = JsonSerializer . Serialize ( written , SettingsManager . JSON_OPTIONS );
var read = JsonSerializer . Deserialize < IDataSource >( json , SettingsManager . JSON_OPTIONS );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( json , Does . Not . Contain ( nameof ( DataSourceLocalDirectory . EffectiveFilePatterns )));
2026-10-27 19:57:04 +01:00
Assert . That ( json , Does . Not . Contain ( nameof ( DataSourceLocalDirectory . EffectiveExcludePatterns )));
2026-10-17 19:12:43 +02:00
Assert . That ( read , Is . TypeOf < DataSourceLocalDirectory >());
Assert . That ((( DataSourceLocalDirectory ) read !). FilePatterns , Is . EqualTo ( "*.txt;*.tf;.gitignore" ));
2026-10-27 19:57:04 +01:00
Assert . That ((( DataSourceLocalDirectory ) read ). ExcludePatterns , Is . EqualTo ( "secret-*;*.backup" ));
2026-10-17 19:12:43 +02:00
Assert . That ((( DataSourceLocalDirectory ) read ). IncludeSubdirectories , Is . False );
Assert . That ((( DataSourceLocalDirectory ) read ). IncludeHiddenFilesAndFolders , Is . True );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
[Test]
public async Task DirectoryStatisticsCanExcludeSubdirectories ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-nonrecursive-statistics-{Guid.NewGuid():N}" );
var subdirectoryPath = Path . Combine ( directoryPath , "nested" );
Directory . CreateDirectory ( subdirectoryPath );
try
{
await File . WriteAllBytesAsync ( Path . Combine ( directoryPath , "included.txt" ), new byte [ 5 ]);
await File . WriteAllBytesAsync ( Path . Combine ( subdirectoryPath , "excluded.txt" ), new byte [ 10 ]);
long totalSize = 0 ;
long fileCount = 0 ;
var reportedFiles = new List < string >();
await new DirectoryInfo ( directoryPath ). DetermineContentSize (
size => totalSize = size ,
count => fileCount = count ,
reportedFiles . Add ,
selection : new FolderFileSelection ([ "*" ], [], false , false , FolderFileSelection . DEFAULT_MAX_FILE_SIZE_MEGABYTES ));
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( totalSize , Is . EqualTo ( 5 ));
Assert . That ( fileCount , Is . EqualTo ( 1 ));
Assert . That ( reportedFiles , Has . Count . EqualTo ( 1 ));
Assert . That ( reportedFiles [ 0 ], Does . EndWith ( "included.txt" ));
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task DirectoryStatisticsUseTheConfiguredHiddenFilesAndFoldersRule ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-hidden-statistics-{Guid.NewGuid():N}" );
var hiddenDirectoryPath = Path . Combine ( directoryPath , ".hidden" );
Directory . CreateDirectory ( hiddenDirectoryPath );
try
{
await File . WriteAllBytesAsync ( Path . Combine ( directoryPath , "visible.txt" ), new byte [ 5 ]);
await File . WriteAllBytesAsync ( Path . Combine ( directoryPath , ".env" ), new byte [ 20 ]);
await File . WriteAllBytesAsync ( Path . Combine ( hiddenDirectoryPath , "hidden.txt" ), new byte [ 10 ]);
long visibleSize = 0 ;
long visibleCount = 0 ;
await new DirectoryInfo ( directoryPath ). DetermineContentSize (
size => visibleSize = size ,
count => visibleCount = count ,
_ => { },
selection : new FolderFileSelection ([ "*" ], [], true , false , FolderFileSelection . DEFAULT_MAX_FILE_SIZE_MEGABYTES ));
long completeSize = 0 ;
long completeCount = 0 ;
await new DirectoryInfo ( directoryPath ). DetermineContentSize (
size => completeSize = size ,
count => completeCount = count ,
_ => { },
selection : new FolderFileSelection ([ "*" ], [], true , true , FolderFileSelection . DEFAULT_MAX_FILE_SIZE_MEGABYTES ));
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( visibleSize , Is . EqualTo ( 5 ));
Assert . That ( visibleCount , Is . EqualTo ( 1 ));
Assert . That ( completeSize , Is . EqualTo ( 35 ));
Assert . That ( completeCount , Is . EqualTo ( 3 ));
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[TestCase("notes.txt")]
[TestCase("main.TF")]
[TestCase(".gitignore")]
[TestCase("nested/folder/variables.tf")]
public void MatchingUsesTheFileName ( string path )
{
var patterns = FileNamePatterns . Parse ( "*.txt;*.tf;.gitignore" );
Assert . That ( FileNamePatterns . Matches ( path , patterns ), Is . True );
}
[TestCase("Dockerfile", "Dockerfile")]
[TestCase("Dockerfile*", "Dockerfile.development")]
[TestCase("Containerfile*", "nested/Containerfile.arm64")]
[TestCase("Jenkinsfile*", "Jenkinsfile.release")]
[TestCase("Makefile", "nested/Makefile")]
[TestCase("GNUmakefile", "GNUmakefile")]
[TestCase("Procfile", "Procfile")]
[TestCase("Vagrantfile", "Vagrantfile")]
[TestCase("Caddyfile*", "Caddyfile.production")]
[TestCase("BUILD;WORKSPACE", "WORKSPACE")]
[TestCase("*.env", ".env")]
[TestCase("deployment notes*#?.txt", "deployment notes (final) [β] # 1. txt ")]
public void GlobPatternsMatchSourceLikeAndUnusualFileNames ( string pattern , string path )
{
Assert . That ( FileNamePatterns . Matches ( path , FileNamePatterns . Parse ( pattern )), Is . True );
}
[Test]
public void GlobPatternsMatchFileNamesAtTheEndOfExtremelyLongPaths ()
{
var directorySegments = Enumerable . Range ( 0 , 8 )
. Select ( index => $"nested-{index}-{new string('x', 32)}" )
. ToArray ();
var path = Path . Combine ([.. directorySegments , "Dockerfile.development" ]);
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( path . Length , Is . GreaterThan ( 300 ));
Assert . That ( FileNamePatterns . Matches ( path , FileNamePatterns . Parse ( "Dockerfile*" )), Is . True );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
[TestCase("image.png")]
[TestCase("notes.txt.bak")]
[TestCase("gitignore")]
public void FilesOutsideThePatternsDoNotMatch ( string path )
{
var patterns = FileNamePatterns . Parse ( "*.txt;*.tf;.gitignore" );
Assert . That ( FileNamePatterns . Matches ( path , patterns ), Is . False );
}
[TestCase(".gitignore")]
[TestCase("LICENSE")]
[TestCase("recording.mp3")]
public void CatchAllPatternIncludesEveryFileName ( string fileName )
{
Assert . That ( FileNamePatterns . Matches ( fileName , FileNamePatterns . Parse ( "*" )), Is . True );
}
[Test]
public async Task ReadableTextStillHasToMatchAFilePattern ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-readable-pattern-{Guid.NewGuid():N}" );
var filePath = Path . Combine ( directoryPath , "main.tf" );
Directory . CreateDirectory ( directoryPath );
try
{
await File . WriteAllTextAsync ( filePath , "resource \"example\" \"main\" {}" );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( FileTypes . IsAllowedPath ( filePath , FileTypes . DOCUMENT ), Is . True );
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( "*.txt" )), Is . False );
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( ":text:" )), Is . True );
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( "*" )), Is . True );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task AllTextFilesPatternRejectsBinaryAndMediaFiles ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-text-pattern-binary-{Guid.NewGuid():N}" );
Directory . CreateDirectory ( directoryPath );
try
{
var binaryPath = Path . Combine ( directoryPath , "archive.data" );
var mediaPath = Path . Combine ( directoryPath , "recording.mp3" );
var imagePath = Path . Combine ( directoryPath , "drawing.svg" );
await File . WriteAllBytesAsync ( binaryPath , [ 0x50 , 0x4B , 0x03 , 0x04 , 0x00 ]);
await File . WriteAllTextAsync ( mediaPath , "This has readable bytes but a media file name." );
await File . WriteAllTextAsync ( imagePath , "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>" );
var patterns = FileNamePatterns . Parse ( ":text:" );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( FileNamePatterns . Matches ( binaryPath , patterns ), Is . False );
Assert . That ( FileNamePatterns . Matches ( mediaPath , patterns ), Is . False );
Assert . That ( FileNamePatterns . Matches ( imagePath , patterns ), Is . False , "An image cannot be indexed, even when it is written as text." );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[TestCase(".env", "*")]
[TestCase(".env", ":text:")]
[TestCase(".env", ":documents:")]
[TestCase(".env", "*.env")]
[TestCase("notes.txt", ".*")]
public void OnlyAPatternWhichBeginsWithADotNamesAHiddenFile ( string fileName , string patterns )
{
Assert . That ( FileNamePatterns . NamesHiddenFile ( fileName , FileNamePatterns . Parse ( patterns )), Is . False );
}
[TestCase(".env", ".env")]
[TestCase(".gitignore", "*.md;.gitignore")]
[TestCase(".env.local", ".env*")]
[TestCase(".editorconfig", ".*")]
public void APatternWhichBeginsWithADotNamesHiddenFiles ( string fileName , string patterns )
{
Assert . That ( FileNamePatterns . NamesHiddenFile ( fileName , FileNamePatterns . Parse ( patterns )), Is . True );
}
[TestCase("report.pdf")]
[TestCase("letter.docx")]
[TestCase("budget.xlsx")]
[TestCase("notes.md")]
[TestCase("Program.cs")]
public void DocumentsSelectorKeepsWhatFolderDataSourcesIndexedBefore ( string fileName )
{
// Known document types are told by their extension, so the file needs no content:
var filePath = Path . Combine ( Path . GetTempPath (), "test-data" , fileName );
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( FileNamePatterns . ALL_DOCUMENTS_PATTERN )), Is . True );
}
[Test]
public async Task DocumentsSelectorAddsReadableTextButNoMediaOrBinaries ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-documents-pattern-{Guid.NewGuid():N}" );
Directory . CreateDirectory ( directoryPath );
try
{
var terraformPath = Path . Combine ( directoryPath , "main.tf" );
var photoPath = Path . Combine ( directoryPath , "photo.png" );
var drawingPath = Path . Combine ( directoryPath , "drawing.svg" );
var recordingPath = Path . Combine ( directoryPath , "recording.mp3" );
var archivePath = Path . Combine ( directoryPath , "archive.zip" );
await File . WriteAllTextAsync ( terraformPath , "resource \"example\" \"main\" {}" );
await File . WriteAllBytesAsync ( photoPath , [ 0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A , 0x00 ]);
await File . WriteAllTextAsync ( drawingPath , "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>" );
await File . WriteAllTextAsync ( recordingPath , "This has readable bytes but a media file name." );
await File . WriteAllBytesAsync ( archivePath , [ 0x50 , 0x4B , 0x03 , 0x04 , 0x00 ]);
var patterns = FileNamePatterns . Parse ( FileNamePatterns . ALL_DOCUMENTS_PATTERN );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( FileNamePatterns . Matches ( terraformPath , patterns ), Is . True , "Readable text is what the selector adds to the documents." );
Assert . That ( FileNamePatterns . Matches ( photoPath , patterns ), Is . False );
Assert . That ( FileNamePatterns . Matches ( drawingPath , patterns ), Is . False , "An image cannot be indexed, even when it is written as text." );
Assert . That ( FileNamePatterns . Matches ( recordingPath , patterns ), Is . False );
Assert . That ( FileNamePatterns . Matches ( archivePath , patterns ), Is . False );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task ExplicitGlobSelectsMatchingBinaryContent ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-explicit-binary-pattern-{Guid.NewGuid():N}" );
var filePath = Path . Combine ( directoryPath , "archive.data" );
Directory . CreateDirectory ( directoryPath );
try
{
await File . WriteAllBytesAsync ( filePath , [ 0x50 , 0x4B , 0x03 , 0x04 , 0x00 ]);
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( FileTypes . IsAllowedPath ( filePath , FileTypes . DOCUMENT ), Is . False );
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( "*.data" )), Is . True );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task DirectoryStatisticsIncludeOnlyMatchingFiles ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-pattern-statistics-{Guid.NewGuid():N}" );
Directory . CreateDirectory ( directoryPath );
try
{
await File . WriteAllBytesAsync ( Path . Combine ( directoryPath , "included.tf" ), new byte [ 5 ]);
await File . WriteAllBytesAsync ( Path . Combine ( directoryPath , "excluded.md" ), new byte [ 10 ]);
long totalSize = 0 ;
long fileCount = 0 ;
var reportedFiles = new List < string >();
var selection = new FolderFileSelection ( FileNamePatterns . Parse ( "*.tf" ), [], true , false , FolderFileSelection . DEFAULT_MAX_FILE_SIZE_MEGABYTES );
await new DirectoryInfo ( directoryPath ). DetermineContentSize (
size => totalSize = size ,
count => fileCount = count ,
reportedFiles . Add ,
selection : selection );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( totalSize , Is . EqualTo ( 5 ));
Assert . That ( fileCount , Is . EqualTo ( 1 ));
Assert . That ( reportedFiles , Has . Count . EqualTo ( 1 ));
Assert . That ( reportedFiles [ 0 ], Does . EndWith ( "included.tf" ));
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[TestCase("*.txt;*.tf;.gitignore")]
[TestCase("*")]
[TestCase(":text:")]
[TestCase(":TEXT:;*.pdf")]
[TestCase(":documents:")]
[TestCase(":DOCUMENTS:;*.tf")]
[TestCase("report-??.md")]
2026-10-27 19:57:04 +01:00
[TestCase("*.txt;")]
[TestCase(";*.txt;; *.tf ; ;")]
[TestCase(":documents:;;")]
2026-10-17 19:12:43 +02:00
public void DataSourceValidationAcceptsSimplePatterns ( string patterns )
{
Assert . That ( new DataSourceValidation (). ValidateFilePatterns ( patterns ), Is . Null );
}
[TestCase("")]
2026-10-27 19:57:04 +01:00
[TestCase(" ; ;; ")]
2026-10-17 19:12:43 +02:00
[TestCase("**/*.txt")]
[TestCase("folder/*.txt")]
[TestCase(":document:")]
[TestCase("*.txt;:docs:")]
2026-10-27 19:57:04 +01:00
[TestCase(";*.txt;; :docs: ;")]
[TestCase("*.txt;;folder/*.md;")]
2026-10-17 19:12:43 +02:00
public void DataSourceValidationRejectsInvalidPatterns ( string patterns )
{
Assert . That ( new DataSourceValidation (). ValidateFilePatterns ( patterns ), Is . Not . Null );
}
}