11using System . Runtime . InteropServices ;
2+ using System . Reflection ;
3+ using System . Text ;
4+ using System . Text . RegularExpressions ;
25using OpusSharp . Core ;
36using Xunit ;
47
@@ -53,6 +56,283 @@ public void DefaultOpusExceptionCtor_ProducesExceptionInstance()
5356 Assert . NotNull ( exception ) ;
5457 }
5558
59+ [ Fact ]
60+ public void MultistreamCtlNoArgumentOverloads_AreManagedWrappers ( )
61+ {
62+ AssertPublicCtlOverloadIsManagedWrapper ( typeof ( NativeOpus ) , nameof ( NativeOpus . opus_ms_encoder_ctl ) , 2 ) ;
63+ AssertPublicCtlOverloadIsManagedWrapper ( typeof ( NativeOpus ) , nameof ( NativeOpus . opus_ms_decoder_ctl ) , 2 ) ;
64+ AssertPublicCtlOverloadIsManagedWrapper ( typeof ( StaticNativeOpus ) , nameof ( StaticNativeOpus . opus_ms_encoder_ctl ) , 2 ) ;
65+ AssertPublicCtlOverloadIsManagedWrapper ( typeof ( StaticNativeOpus ) , nameof ( StaticNativeOpus . opus_ms_decoder_ctl ) , 2 ) ;
66+
67+ AssertPrivateLibraryImportSymbol ( typeof ( NativeOpus ) , "opus_multistream_encoder_ctl" ) ;
68+ AssertPrivateLibraryImportSymbol ( typeof ( NativeOpus ) , "opus_multistream_decoder_ctl" ) ;
69+ AssertPrivateLibraryImportSymbol ( typeof ( StaticNativeOpus ) , "opus_multistream_encoder_ctl" ) ;
70+ AssertPrivateLibraryImportSymbol ( typeof ( StaticNativeOpus ) , "opus_multistream_decoder_ctl" ) ;
71+ }
72+
73+ [ Fact ]
74+ public void CtlOverloadsWithVariadicArguments_AreManagedWrappers ( )
75+ {
76+ foreach ( var method in GetPublicCtlMethods ( typeof ( NativeOpus ) , typeof ( StaticNativeOpus ) )
77+ . Where ( method => method . GetParameters ( ) . Length > 2 ) )
78+ {
79+ Assert . Empty ( method . GetCustomAttributes < LibraryImportAttribute > ( ) ) ;
80+ Assert . Empty ( method . GetCustomAttributes < DllImportAttribute > ( ) ) ;
81+ }
82+ }
83+
84+ [ Fact ]
85+ public void ManagedCtlShimImports_AreDeclaredInNativeShimSource ( )
86+ {
87+ var shimSource = File . ReadAllText ( Path . Combine (
88+ TestNativeLibraryBootstrapper . GetRepositoryRoot ( ) ,
89+ "OpusSharp.Natives" ,
90+ "opus_shim.c" ) ) ;
91+
92+ foreach ( var symbol in GetManagedCtlShimSymbols ( ) )
93+ {
94+ Assert . Contains ( $ "SHIM_EXPORT int { symbol } (", shimSource ) ;
95+ }
96+ }
97+
98+ [ Fact ]
99+ public void NativeRuntimeLibrary_ExportsManagedCtlShimSymbols ( )
100+ {
101+ var libraryPath = TestNativeLibraryBootstrapper . GetOutputNativeLibraryPath ( ) ;
102+ var handle = NativeLibrary . Load ( libraryPath ) ;
103+
104+ try
105+ {
106+ foreach ( var symbol in GetManagedCtlShimSymbols ( ) )
107+ {
108+ Assert . True (
109+ NativeLibrary . TryGetExport ( handle , symbol , out _ ) ,
110+ $ "Expected '{ Path . GetFileName ( libraryPath ) } ' to export '{ symbol } '.") ;
111+ }
112+ }
113+ finally
114+ {
115+ NativeLibrary . Free ( handle ) ;
116+ }
117+ }
118+
119+ [ Fact ]
120+ public void IosNativeArchives_ContainManagedCtlShimSymbols ( )
121+ {
122+ var iosNativeRoot = Path . Combine (
123+ TestNativeLibraryBootstrapper . GetRepositoryRoot ( ) ,
124+ "OpusSharp.Natives" ,
125+ "runtimes" ,
126+ "ios" ,
127+ "native" ,
128+ "libopus.xcframework" ) ;
129+ var archivePaths = Directory . GetFiles ( iosNativeRoot , "libopus.a" , SearchOption . AllDirectories ) ;
130+
131+ Assert . NotEmpty ( archivePaths ) ;
132+
133+ foreach ( var archivePath in archivePaths )
134+ {
135+ foreach ( var symbol in GetManagedCtlShimSymbols ( ) )
136+ {
137+ AssertFileContainsAsciiSymbol ( archivePath , symbol ) ;
138+ }
139+ }
140+ }
141+
142+ [ Fact ]
143+ public void SupportedNativeRuntimeLibraries_ContainManagedCtlShimSymbols ( )
144+ {
145+ var nativeFiles = GetSupportedNativeRuntimeFiles ( ) . ToArray ( ) ;
146+
147+ Assert . NotEmpty ( nativeFiles ) ;
148+
149+ foreach ( var nativeFile in nativeFiles )
150+ {
151+ foreach ( var symbol in GetManagedCtlShimSymbols ( ) )
152+ {
153+ AssertFileContainsAsciiSymbol ( nativeFile , symbol ) ;
154+ }
155+ }
156+ }
157+
158+ [ Fact ]
159+ public void PackageSmoke_UsesCurrentPackageVersion ( )
160+ {
161+ var props = File . ReadAllText ( Path . Combine (
162+ TestNativeLibraryBootstrapper . GetRepositoryRoot ( ) ,
163+ "samples" ,
164+ "PackageSmoke" ,
165+ "Directory.Build.props" ) ) ;
166+
167+ Assert . Contains ( @"<Import Project=""..\..\Directory.Build.props"" />" , props ) ;
168+ Assert . Contains ( "<OpusSharpSmokeVersion>$(OpusSharpVersion)</OpusSharpSmokeVersion>" , props ) ;
169+ }
170+
171+ [ Fact ]
172+ public void NativesPackage_ExcludesUnsupportedWindowsRuntimes ( )
173+ {
174+ var project = File . ReadAllText ( Path . Combine (
175+ TestNativeLibraryBootstrapper . GetRepositoryRoot ( ) ,
176+ "OpusSharp.Natives" ,
177+ "OpusSharp.Natives.csproj" ) ) ;
178+
179+ Assert . Contains ( "runtimes\\ win-arm\\ **\\ *" , project ) ;
180+ Assert . Contains ( "runtimes\\ win-x86\\ **\\ *" , project ) ;
181+ }
182+
183+ [ Fact ]
184+ public void WindowsNativeBuildWorkflow_HandlesDecoratedX86OpusSymbols ( )
185+ {
186+ var workflow = File . ReadAllText ( Path . Combine (
187+ TestNativeLibraryBootstrapper . GetRepositoryRoot ( ) ,
188+ ".github" ,
189+ "workflows" ,
190+ "OpusCompile.yml" ) ) ;
191+
192+ Assert . Contains ( "_?opus_\\ S+" , workflow ) ;
193+ Assert . Contains ( "$raw.Substring(1) + '=' + $raw" , workflow ) ;
194+ }
195+
196+ private static void AssertPublicCtlOverloadIsManagedWrapper ( Type type , string methodName , int parameterCount )
197+ {
198+ var method = Assert . Single (
199+ type . GetMethods ( BindingFlags . Public | BindingFlags . Static ) ,
200+ method => method . Name == methodName && method . GetParameters ( ) . Length == parameterCount ) ;
201+
202+ Assert . Empty ( method . GetCustomAttributes < LibraryImportAttribute > ( ) ) ;
203+ Assert . Empty ( method . GetCustomAttributes < DllImportAttribute > ( ) ) ;
204+ }
205+
206+ private static void AssertPrivateLibraryImportSymbol ( Type type , string methodName )
207+ {
208+ var method = Assert . Single (
209+ type . GetMethods ( BindingFlags . NonPublic | BindingFlags . Static ) ,
210+ method => method . Name == methodName && method . GetParameters ( ) . Length == 2 ) ;
211+
212+ Assert . Equal ( methodName , GetImportedSymbol ( method ) ) ;
213+ }
214+
215+ private static IEnumerable < MethodInfo > GetPublicCtlMethods ( params Type [ ] types )
216+ {
217+ return types . SelectMany ( type => type . GetMethods ( BindingFlags . Public | BindingFlags . Static ) )
218+ . Where ( method =>
219+ method . Name . StartsWith ( "opus_" , StringComparison . Ordinal ) &&
220+ method . Name . EndsWith ( "_ctl" , StringComparison . Ordinal ) ) ;
221+ }
222+
223+ private static IEnumerable < string > GetManagedCtlShimSymbols ( )
224+ {
225+ return GetCtlImportMethods ( typeof ( NativeOpus ) , typeof ( StaticNativeOpus ) )
226+ . Select ( GetImportedSymbol )
227+ . Where ( symbol => symbol . StartsWith ( "opussharp_" , StringComparison . Ordinal ) )
228+ . Distinct ( )
229+ . Order ( StringComparer . Ordinal ) ;
230+ }
231+
232+ private static IEnumerable < MethodInfo > GetCtlImportMethods ( params Type [ ] types )
233+ {
234+ return types . SelectMany ( type => type . GetMethods (
235+ BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static ) )
236+ . Where ( method =>
237+ method . Name . Contains ( "_ctl" , StringComparison . Ordinal ) &&
238+ method . GetCustomAttributes < LibraryImportAttribute > ( ) . Any ( ) ) ;
239+ }
240+
241+ private static string GetImportedSymbol ( MethodInfo method )
242+ {
243+ var importAttribute = Assert . Single ( method . GetCustomAttributes < LibraryImportAttribute > ( ) ) ;
244+
245+ return string . IsNullOrEmpty ( importAttribute . EntryPoint )
246+ ? method . Name
247+ : importAttribute . EntryPoint ;
248+ }
249+
250+ private static IEnumerable < string > GetSupportedNativeRuntimeFiles ( )
251+ {
252+ var nativesRoot = Path . Combine (
253+ TestNativeLibraryBootstrapper . GetRepositoryRoot ( ) ,
254+ "OpusSharp.Natives" ) ;
255+ var runtimesRoot = Path . Combine ( nativesRoot , "runtimes" ) ;
256+ var readmePath = Path . Combine ( nativesRoot , "README.md" ) ;
257+ var runtimePathPattern = new Regex ( @"^- [^:]+: (?<path>\S+)$" , RegexOptions . CultureInvariant ) ;
258+
259+ foreach ( var line in File . ReadLines ( readmePath ) )
260+ {
261+ var match = runtimePathPattern . Match ( line ) ;
262+
263+ if ( ! match . Success )
264+ {
265+ continue ;
266+ }
267+
268+ var relativePath = match . Groups [ "path" ] . Value . Replace ( '/' , Path . DirectorySeparatorChar ) ;
269+ var fullPath = Path . Combine ( runtimesRoot , relativePath ) ;
270+
271+ if ( File . Exists ( fullPath ) )
272+ {
273+ if ( IsNativeLibraryFile ( fullPath ) )
274+ {
275+ yield return fullPath ;
276+ }
277+
278+ continue ;
279+ }
280+
281+ Assert . True ( Directory . Exists ( fullPath ) , $ "Expected supported native runtime path '{ fullPath } ' to exist.") ;
282+
283+ foreach ( var nativeFile in Directory . GetFiles ( fullPath , "*" , SearchOption . AllDirectories )
284+ . Where ( IsNativeLibraryFile ) )
285+ {
286+ yield return nativeFile ;
287+ }
288+ }
289+ }
290+
291+ private static bool IsNativeLibraryFile ( string filePath )
292+ {
293+ return Path . GetExtension ( filePath ) switch
294+ {
295+ ".a" or ".dll" or ".dylib" or ".so" => true ,
296+ _ => false
297+ } ;
298+ }
299+
300+ private static void AssertFileContainsAsciiSymbol ( string filePath , string symbol )
301+ {
302+ var fileBytes = File . ReadAllBytes ( filePath ) ;
303+ var symbolBytes = Encoding . ASCII . GetBytes ( symbol ) ;
304+
305+ Assert . True (
306+ IndexOf ( fileBytes , symbolBytes ) >= 0 ,
307+ $ "Expected '{ filePath } ' to contain symbol '{ symbol } '.") ;
308+ }
309+
310+ private static int IndexOf ( byte [ ] source , byte [ ] value )
311+ {
312+ for ( var i = 0 ; i <= source . Length - value . Length ; i ++ )
313+ {
314+ var found = true ;
315+
316+ for ( var j = 0 ; j < value . Length ; j ++ )
317+ {
318+ if ( source [ i + j ] == value [ j ] )
319+ {
320+ continue ;
321+ }
322+
323+ found = false ;
324+ break ;
325+ }
326+
327+ if ( found )
328+ {
329+ return i ;
330+ }
331+ }
332+
333+ return - 1 ;
334+ }
335+
56336 private static class TestNativeLibraryBootstrapper
57337 {
58338 private static bool _initialized ;
@@ -75,12 +355,29 @@ public static void EnsureInitialized()
75355 _initialized = true ;
76356 }
77357
358+ public static string GetRepositoryRoot ( )
359+ {
360+ return Path . GetFullPath ( Path . Combine ( AppContext . BaseDirectory , "../../../../" ) ) ;
361+ }
362+
363+ public static string GetOutputNativeLibraryPath ( )
364+ {
365+ EnsureInitialized ( ) ;
366+
367+ return Path . Combine ( AppContext . BaseDirectory , Path . GetFileName ( GetNativeLibraryPath ( ) ) ) ;
368+ }
369+
78370 private static string GetNativeLibraryPath ( )
79371 {
80- var repoRoot = Path . GetFullPath ( Path . Combine ( AppContext . BaseDirectory , "../../../../" ) ) ;
81372 var runtimeFolder = GetRuntimeFolder ( ) ;
82373 var fileName = GetNativeLibraryFileName ( ) ;
83- var path = Path . Combine ( repoRoot , "OpusSharp.Natives" , "runtimes" , runtimeFolder , "native" , fileName ) ;
374+ var path = Path . Combine (
375+ GetRepositoryRoot ( ) ,
376+ "OpusSharp.Natives" ,
377+ "runtimes" ,
378+ runtimeFolder ,
379+ "native" ,
380+ fileName ) ;
84381
85382 if ( ! File . Exists ( path ) )
86383 {
0 commit comments