├── .editorconfig ├── .gitignore ├── .repoconfig ├── README.md ├── format.ps1 ├── notes ├── ECMA-335.pdf ├── variations.cs └── variations.linq ├── source ├── .gitignore ├── .idea │ └── .idea.NSubstitute.Elevated │ │ ├── .idea │ │ ├── .name │ │ ├── encodings.xml │ │ ├── indexLayout.xml │ │ └── modules.xml │ │ └── riderModule.iml ├── NSubstitute.Elevated.sln ├── NSubstitute.Elevated.sln.DotSettings ├── NSubstitute.Elevated │ ├── AssemblyInfo.cs │ ├── ElevatedSubstituteManager.cs │ ├── ElevatedSubstitutionContext.cs │ ├── NSubstitute.Elevated.csproj │ ├── PatchedAssemblyBridge.cs │ ├── SubstituteStatic.cs │ └── Weaver │ │ ├── CecilExtensions.cs │ │ ├── ElevatedWeaver.cs │ │ ├── MockInjector.cs │ │ └── PeVerify.cs ├── Unity.Core.Tests │ ├── DictionaryExtensionsTests.cs │ ├── DiffUtilsTests.cs │ ├── EnumUtilityTests.cs │ ├── EnumerableExtensionsTests.cs │ ├── ExtensionsTests.cs │ ├── StringExtensionsTests.cs │ └── Unity.Core.Tests.csproj ├── Unity.Core │ ├── ConsoleUtils.cs │ ├── DictionaryExtensions.cs │ ├── DiffUtils.cs │ ├── DisposableUtils.cs │ ├── EnumUtility.cs │ ├── EnumerableExtensions.cs │ ├── Extensions.cs │ ├── HasParent.cs │ ├── ILogger.cs │ ├── NiceIO │ │ └── NiceIO.cs │ ├── ProcessUtility.cs │ ├── SafeFile.cs │ ├── StringExtensions.cs │ └── Unity.Core.csproj └── common.targets └── tests ├── NSubstitute.Elevated.Tests ├── BasicTests.cs ├── ElevatedWeaverTests.cs ├── NSubstitute.Elevated.Tests.csproj ├── PeVerifyTests.cs └── Utilities │ ├── PatchingFixture.cs │ └── TestFileSystemFixture.cs └── Support ├── DependentAssembly ├── DependentAssembly.csproj └── DependentType.cs └── SystemUnderTest ├── BasicTypes.cs └── SystemUnderTest.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org/ for docs on this file 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf ; help with sharing files across os's (i.e. network share or through local vm) 9 | #charset temporarily disabled due to bug in VS2017 changing to UTF-8 with BOM (https://favro.com/card/c564ede4ed3337f7b17986b6/Uni-17877) 10 | #charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | # trailing whitespace is significant in markdown (bad choice, bad!) 15 | [*.{md,markdown}] 16 | trim_trailing_whitespace = false 17 | 18 | # keep these and the VS stuff below in sync with .hgeol's CRLF extensions 19 | [*.{vcproj,bat,cmd,xaml,tt,t4,ttinclude}] 20 | end_of_line = crlf 21 | 22 | # this VS-specific stuff is based on experiments to see how VS will modify a file after it has been manually edited. 23 | # the settings are meant to closely match what VS does to minimize unnecessary diffs. this duplicates some settings in * 24 | # but let's be explicit here to be safe (in case someone wants to copy-paste this out to another .editorconfig). 25 | [*.{vcxproj,vcxproj.filters}] 26 | indent_style = space 27 | indent_size = 2 28 | end_of_line = crlf 29 | charset = utf-8-bom 30 | trim_trailing_whitespace = true 31 | insert_final_newline = false 32 | # must be broken out because of 51-char bug (https://github.com/editorconfig/editorconfig-visualstudio/issues/21) 33 | [*.{csproj,pyproj,props,targets}] 34 | indent_style = space 35 | indent_size = 2 36 | end_of_line = crlf 37 | charset = utf-8-bom 38 | trim_trailing_whitespace = true 39 | insert_final_newline = false 40 | [*.{sln,sln.template}] 41 | indent_style = tab 42 | indent_size = 4 43 | end_of_line = crlf 44 | charset = utf-8 45 | trim_trailing_whitespace = true 46 | insert_final_newline = false 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | **/bin/Debug 3 | **/bin/Release 4 | -------------------------------------------------------------------------------- /.repoconfig: -------------------------------------------------------------------------------- 1 | [version] 2 | repoconfig=4 3 | 4 | [format] 5 | path-ignore=<( 166 | // !!0/*T*/ count 167 | // ) cil managed 168 | public void VoidGenericMethodPatched(/*Parameter with token 0800000B*/T count) 169 | // .maxstack 9 170 | // .locals init ( 171 | // [0] object 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0016 ldloca.s))]', 172 | // [1] bool V_1 173 | // ) 174 | // 175 | // IL_0000: nop 176 | // IL_0001: ldtoken UserQuery/SimpleClass 177 | // IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 178 | // IL_000b: ldarg.0 // this 179 | // IL_000c: ldtoken [mscorlib]System.Int32 180 | // IL_0011: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 181 | // IL_0016: ldloca.s 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0016 ldloca.s))]' 182 | // IL_0018: ldsfld class [mscorlib]System.Type[] [mscorlib]System.Type::EmptyTypes 183 | // IL_001d: ldc.i4.1 184 | // IL_001e: newarr [mscorlib]System.Object 185 | // IL_0023: dup 186 | // IL_0024: ldc.i4.0 187 | // IL_0025: ldarg.1 // count 188 | // IL_0026: box !!0/*T*/ 189 | // IL_002b: stelem.ref 190 | // IL_002c: call bool UserQuery/PatchedAssemblyBridge::TryMock(class [mscorlib]System.Type, object, class [mscorlib]System.Type, object&, class [mscorlib]System.Type[], object[]) 191 | // IL_0031: stloc.1 // V_1 192 | // IL_0032: ldloc.1 // V_1 193 | // IL_0033: brfalse.s IL_0037 194 | // IL_0035: br.s IL_0045 195 | // IL_0037: ldarg.0 // this 196 | // IL_0038: ldarg.0 // this 197 | // IL_0039: ldfld int32 UserQuery/SimpleClass::Modified 198 | // IL_003e: ldc.i4.1 199 | // IL_003f: add 200 | // IL_0040: stfld int32 UserQuery/SimpleClass::Modified 201 | // IL_0045: ret 202 | // 203 | { 204 | object mockedReturnValue; 205 | if (UserQuery.PatchedAssemblyBridge.TryMock(typeof (UserQuery.SimpleClass), (object) this, typeof (int), out mockedReturnValue, Type.EmptyTypes, new object[1] 206 | { 207 | (object) count 208 | })) 209 | return; 210 | this.Modified = this.Modified + 1; 211 | } 212 | 213 | /*Method VoidGenericMethod with token 0600000A*/ 214 | // .method public hidebysig instance void 215 | // VoidGenericMethod( 216 | // !!0/*T*/ count 217 | // ) cil managed 218 | public void VoidGenericMethod(/*Parameter with token 0800000C*/T count) 219 | // .maxstack 8 220 | // 221 | // IL_0000: nop 222 | // IL_0001: ldarg.0 // this 223 | // IL_0002: ldarg.0 // this 224 | // IL_0003: ldfld int32 UserQuery/SimpleClass::Modified 225 | // IL_0008: ldc.i4.1 226 | // IL_0009: add 227 | // IL_000a: stfld int32 UserQuery/SimpleClass::Modified 228 | // IL_000f: ret 229 | // 230 | { 231 | this.Modified = this.Modified + 1; 232 | } 233 | 234 | /*Method GenericMethodPatched with token 0600000B*/ 235 | // .method public hidebysig instance !!0/*T*/ 236 | // GenericMethodPatched( 237 | // !!0/*T*/ count 238 | // ) cil managed 239 | public T GenericMethodPatched(/*Parameter with token 0800000D*/T count) 240 | // .maxstack 9 241 | // .locals init ( 242 | // [0] object 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]', 243 | // [1] bool V_1, 244 | // [2] !!0/*T*/ V_2 245 | // ) 246 | // 247 | // IL_0000: nop 248 | // IL_0001: ldtoken UserQuery/SimpleClass 249 | // IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 250 | // IL_000b: ldarg.0 // this 251 | // IL_000c: ldtoken [mscorlib]System.Int32 252 | // IL_0011: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 253 | // IL_0016: ldloca.s 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]' 254 | // IL_0018: ldsfld class [mscorlib]System.Type[] [mscorlib]System.Type::EmptyTypes 255 | // IL_001d: ldc.i4.1 256 | // IL_001e: newarr [mscorlib]System.Object 257 | // IL_0023: dup 258 | // IL_0024: ldc.i4.0 259 | // IL_0025: ldarg.1 // count 260 | // IL_0026: box !!0/*T*/ 261 | // IL_002b: stelem.ref 262 | // IL_002c: call bool UserQuery/PatchedAssemblyBridge::TryMock(class [mscorlib]System.Type, object, class [mscorlib]System.Type, object&, class [mscorlib]System.Type[], object[]) 263 | // IL_0031: stloc.1 // V_1 264 | // IL_0032: ldloc.1 // V_1 265 | // IL_0033: brfalse.s IL_003e 266 | // IL_0035: ldloc.0 // 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]' 267 | // IL_0036: unbox.any !!0/*T*/ 268 | // IL_003b: stloc.2 // V_2 269 | // IL_003c: br.s IL_0050 270 | // IL_003e: ldarg.0 // this 271 | // IL_003f: ldarg.0 // this 272 | // IL_0040: ldfld int32 UserQuery/SimpleClass::Modified 273 | // IL_0045: ldc.i4.1 274 | // IL_0046: add 275 | // IL_0047: stfld int32 UserQuery/SimpleClass::Modified 276 | // IL_004c: ldarg.1 // count 277 | // IL_004d: stloc.2 // V_2 278 | // IL_004e: br.s IL_0050 279 | // IL_0050: ldloc.2 // V_2 280 | // IL_0051: ret 281 | // 282 | { 283 | object mockedReturnValue; 284 | if (UserQuery.PatchedAssemblyBridge.TryMock(typeof (UserQuery.SimpleClass), (object) this, typeof (int), out mockedReturnValue, Type.EmptyTypes, new object[1] 285 | { 286 | (object) count 287 | })) 288 | return (T) mockedReturnValue; 289 | this.Modified = this.Modified + 1; 290 | return count; 291 | } 292 | 293 | /*Method GenericMethod with token 0600000C*/ 294 | // .method public hidebysig instance !!0/*T*/ 295 | // GenericMethod( 296 | // !!0/*T*/ count 297 | // ) cil managed 298 | public T GenericMethod(/*Parameter with token 0800000E*/T count) 299 | // .maxstack 3 300 | // .locals init ( 301 | // [0] !!0/*T*/ V_0 302 | // ) 303 | // 304 | // IL_0000: nop 305 | // IL_0001: ldarg.0 // this 306 | // IL_0002: ldarg.0 // this 307 | // IL_0003: ldfld int32 UserQuery/SimpleClass::Modified 308 | // IL_0008: ldc.i4.1 309 | // IL_0009: add 310 | // IL_000a: stfld int32 UserQuery/SimpleClass::Modified 311 | // IL_000f: ldarg.1 // count 312 | // IL_0010: stloc.0 // V_0 313 | // IL_0011: br.s IL_0013 314 | // IL_0013: ldloc.0 // V_0 315 | // IL_0014: ret 316 | // 317 | { 318 | this.Modified = this.Modified + 1; 319 | return count; 320 | } 321 | 322 | /*Method .ctor with token 0600000D*/ 323 | // .method public hidebysig specialname rtspecialname instance void 324 | // .ctor() cil managed 325 | public SimpleClass() 326 | // .maxstack 8 327 | // 328 | // IL_0000: ldarg.0 // this 329 | // IL_0001: call instance void [mscorlib]System.Object::.ctor() 330 | // IL_0006: nop 331 | // IL_0007: ret 332 | // 333 | { 334 | base.\u002Ector(); 335 | } 336 | } 337 | 338 | /*Type GenericClass`1 with token 02000006*/ 339 | // .class nested public auto ansi beforefieldinit 340 | // GenericClass`1 341 | // extends [mscorlib]System.Object 342 | // 343 | public class GenericClass 344 | { 345 | /*Field Modified with token 04000002*/ 346 | // .field public int32 Modified 347 | public int Modified; 348 | 349 | /*Method VoidMethod with token 0600000E*/ 350 | // .method public hidebysig instance void 351 | // VoidMethod( 352 | // int32 count 353 | // ) cil managed 354 | public void VoidMethod(/*Parameter with token 0800000F*/int count) 355 | // .maxstack 9 356 | // .locals init ( 357 | // [0] bool V_0, 358 | // [1] object 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0016 ldloca.s))]' 359 | // ) 360 | // 361 | // IL_0000: nop 362 | // IL_0001: ldtoken UserQuery/SimpleClass 363 | // IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 364 | // IL_000b: ldarg.0 // this 365 | // IL_000c: ldtoken [mscorlib]System.Void 366 | // IL_0011: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 367 | // IL_0016: ldloca.s 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0016 ldloca.s))]' 368 | // IL_0018: ldsfld class [mscorlib]System.Type[] [mscorlib]System.Type::EmptyTypes 369 | // IL_001d: ldc.i4.1 370 | // IL_001e: newarr [mscorlib]System.Object 371 | // IL_0023: dup 372 | // IL_0024: ldc.i4.0 373 | // IL_0025: ldarg.1 // count 374 | // IL_0026: box [mscorlib]System.Int32 375 | // IL_002b: stelem.ref 376 | // IL_002c: call bool UserQuery/PatchedAssemblyBridge::TryMock(class [mscorlib]System.Type, object, class [mscorlib]System.Type, object&, class [mscorlib]System.Type[], object[]) 377 | // IL_0031: stloc.0 // V_0 378 | // IL_0032: ldloc.0 // V_0 379 | // IL_0033: brfalse.s IL_0037 380 | // IL_0035: br.s IL_0045 381 | // IL_0037: ldarg.0 // this 382 | // IL_0038: ldarg.0 // this 383 | // IL_0039: ldfld int32 class UserQuery/GenericClass`1::Modified 384 | // IL_003e: ldarg.1 // count 385 | // IL_003f: add 386 | // IL_0040: stfld int32 class UserQuery/GenericClass`1::Modified 387 | // IL_0045: ret 388 | // 389 | { 390 | object mockedReturnValue; 391 | if (UserQuery.PatchedAssemblyBridge.TryMock(typeof (UserQuery.SimpleClass), (object) this, typeof (void), out mockedReturnValue, Type.EmptyTypes, new object[1] 392 | { 393 | (object) count 394 | })) 395 | return; 396 | this.Modified = this.Modified + count; 397 | } 398 | 399 | /*Method ReturnMethod with token 0600000F*/ 400 | // .method public hidebysig instance int32 401 | // ReturnMethod( 402 | // int32 count 403 | // ) cil managed 404 | public int ReturnMethod(/*Parameter with token 08000010*/int count) 405 | // .maxstack 9 406 | // .locals init ( 407 | // [0] object 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]', 408 | // [1] bool V_1, 409 | // [2] int32 V_2, 410 | // [3] int32 V_3 411 | // ) 412 | // 413 | // IL_0000: nop 414 | // IL_0001: ldtoken UserQuery/SimpleClass 415 | // IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 416 | // IL_000b: ldarg.0 // this 417 | // IL_000c: ldtoken [mscorlib]System.Int32 418 | // IL_0011: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 419 | // IL_0016: ldloca.s 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]' 420 | // IL_0018: ldsfld class [mscorlib]System.Type[] [mscorlib]System.Type::EmptyTypes 421 | // IL_001d: ldc.i4.1 422 | // IL_001e: newarr [mscorlib]System.Object 423 | // IL_0023: dup 424 | // IL_0024: ldc.i4.0 425 | // IL_0025: ldarg.1 // count 426 | // IL_0026: box [mscorlib]System.Int32 427 | // IL_002b: stelem.ref 428 | // IL_002c: call bool UserQuery/PatchedAssemblyBridge::TryMock(class [mscorlib]System.Type, object, class [mscorlib]System.Type, object&, class [mscorlib]System.Type[], object[]) 429 | // IL_0031: stloc.1 // V_1 430 | // IL_0032: ldloc.1 // V_1 431 | // IL_0033: brfalse.s IL_003e 432 | // IL_0035: ldloc.0 // 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]' 433 | // IL_0036: unbox.any [mscorlib]System.Int32 434 | // IL_003b: stloc.2 // V_2 435 | // IL_003c: br.s IL_0052 436 | // IL_003e: ldarg.0 // this 437 | // IL_003f: ldarg.0 // this 438 | // IL_0040: ldfld int32 class UserQuery/GenericClass`1::Modified 439 | // IL_0045: ldarg.1 // count 440 | // IL_0046: add 441 | // IL_0047: dup 442 | // IL_0048: stloc.3 // V_3 443 | // IL_0049: stfld int32 class UserQuery/GenericClass`1::Modified 444 | // IL_004e: ldloc.3 // V_3 445 | // IL_004f: stloc.2 // V_2 446 | // IL_0050: br.s IL_0052 447 | // IL_0052: ldloc.2 // V_2 448 | // IL_0053: ret 449 | // 450 | { 451 | object mockedReturnValue; 452 | if (UserQuery.PatchedAssemblyBridge.TryMock(typeof (UserQuery.SimpleClass), (object) this, typeof (int), out mockedReturnValue, Type.EmptyTypes, new object[1] 453 | { 454 | (object) count 455 | })) 456 | return (int) mockedReturnValue; 457 | return this.Modified = this.Modified + count; 458 | } 459 | 460 | /*Method VoidGenericMethod with token 06000010*/ 461 | // .method public hidebysig instance void 462 | // VoidGenericMethod( 463 | // !!0/*T*/ count 464 | // ) cil managed 465 | public void VoidGenericMethod(/*Parameter with token 08000011*/T count) 466 | // .maxstack 9 467 | // .locals init ( 468 | // [0] object 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0016 ldloca.s))]', 469 | // [1] bool V_1 470 | // ) 471 | // 472 | // IL_0000: nop 473 | // IL_0001: ldtoken UserQuery/SimpleClass 474 | // IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 475 | // IL_000b: ldarg.0 // this 476 | // IL_000c: ldtoken [mscorlib]System.Int32 477 | // IL_0011: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 478 | // IL_0016: ldloca.s 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0016 ldloca.s))]' 479 | // IL_0018: ldsfld class [mscorlib]System.Type[] [mscorlib]System.Type::EmptyTypes 480 | // IL_001d: ldc.i4.1 481 | // IL_001e: newarr [mscorlib]System.Object 482 | // IL_0023: dup 483 | // IL_0024: ldc.i4.0 484 | // IL_0025: ldarg.1 // count 485 | // IL_0026: box !!0/*T*/ 486 | // IL_002b: stelem.ref 487 | // IL_002c: call bool UserQuery/PatchedAssemblyBridge::TryMock(class [mscorlib]System.Type, object, class [mscorlib]System.Type, object&, class [mscorlib]System.Type[], object[]) 488 | // IL_0031: stloc.1 // V_1 489 | // IL_0032: ldloc.1 // V_1 490 | // IL_0033: brfalse.s IL_0037 491 | // IL_0035: br.s IL_0045 492 | // IL_0037: ldarg.0 // this 493 | // IL_0038: ldarg.0 // this 494 | // IL_0039: ldfld int32 class UserQuery/GenericClass`1::Modified 495 | // IL_003e: ldc.i4.1 496 | // IL_003f: add 497 | // IL_0040: stfld int32 class UserQuery/GenericClass`1::Modified 498 | // IL_0045: ret 499 | // 500 | { 501 | object mockedReturnValue; 502 | if (UserQuery.PatchedAssemblyBridge.TryMock(typeof (UserQuery.SimpleClass), (object) this, typeof (int), out mockedReturnValue, Type.EmptyTypes, new object[1] 503 | { 504 | (object) count 505 | })) 506 | return; 507 | this.Modified = this.Modified + 1; 508 | } 509 | 510 | /*Method GenericMethod with token 06000011*/ 511 | // .method public hidebysig instance !!0/*T*/ 512 | // GenericMethod( 513 | // !!0/*T*/ count 514 | // ) cil managed 515 | public T GenericMethod(/*Parameter with token 08000012*/T count) 516 | // .maxstack 9 517 | // .locals init ( 518 | // [0] object 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]', 519 | // [1] bool V_1, 520 | // [2] !!0/*T*/ V_2 521 | // ) 522 | // 523 | // IL_0000: nop 524 | // IL_0001: ldtoken UserQuery/SimpleClass 525 | // IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 526 | // IL_000b: ldarg.0 // this 527 | // IL_000c: ldtoken [mscorlib]System.Int32 528 | // IL_0011: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 529 | // IL_0016: ldloca.s 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]' 530 | // IL_0018: ldsfld class [mscorlib]System.Type[] [mscorlib]System.Type::EmptyTypes 531 | // IL_001d: ldc.i4.1 532 | // IL_001e: newarr [mscorlib]System.Object 533 | // IL_0023: dup 534 | // IL_0024: ldc.i4.0 535 | // IL_0025: ldarg.1 // count 536 | // IL_0026: box !!0/*T*/ 537 | // IL_002b: stelem.ref 538 | // IL_002c: call bool UserQuery/PatchedAssemblyBridge::TryMock(class [mscorlib]System.Type, object, class [mscorlib]System.Type, object&, class [mscorlib]System.Type[], object[]) 539 | // IL_0031: stloc.1 // V_1 540 | // IL_0032: ldloc.1 // V_1 541 | // IL_0033: brfalse.s IL_003e 542 | // IL_0035: ldloc.0 // 'mockedReturnValue [Range(Instruction(IL_0016 ldloca.s)-Instruction(IL_0035 ldloc.0))]' 543 | // IL_0036: unbox.any !!0/*T*/ 544 | // IL_003b: stloc.2 // V_2 545 | // IL_003c: br.s IL_0050 546 | // IL_003e: ldarg.0 // this 547 | // IL_003f: ldarg.0 // this 548 | // IL_0040: ldfld int32 class UserQuery/GenericClass`1::Modified 549 | // IL_0045: ldc.i4.1 550 | // IL_0046: add 551 | // IL_0047: stfld int32 class UserQuery/GenericClass`1::Modified 552 | // IL_004c: ldarg.1 // count 553 | // IL_004d: stloc.2 // V_2 554 | // IL_004e: br.s IL_0050 555 | // IL_0050: ldloc.2 // V_2 556 | // IL_0051: ret 557 | // 558 | { 559 | object mockedReturnValue; 560 | if (UserQuery.PatchedAssemblyBridge.TryMock(typeof (UserQuery.SimpleClass), (object) this, typeof (int), out mockedReturnValue, Type.EmptyTypes, new object[1] 561 | { 562 | (object) count 563 | })) 564 | return (T) mockedReturnValue; 565 | this.Modified = this.Modified + 1; 566 | return count; 567 | } 568 | 569 | /*Method .ctor with token 06000012*/ 570 | // .method public hidebysig specialname rtspecialname instance void 571 | // .ctor() cil managed 572 | public GenericClass() 573 | // .maxstack 8 574 | // 575 | // IL_0000: ldarg.0 // this 576 | // IL_0001: call instance void [mscorlib]System.Object::.ctor() 577 | // IL_0006: nop 578 | // IL_0007: ret 579 | // 580 | { 581 | base.\u002Ector(); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /notes/variations.linq: -------------------------------------------------------------------------------- 1 | 2 | 3 | void Main() 4 | { 5 | Assembly.GetExecutingAssembly().Location.Dump(); 6 | } 7 | 8 | // purpose of this file is to exercise all patched and unpatched variations, for examining the 9 | // compiled IL and having the patcher generate it. 10 | // 11 | // variations to apply: 12 | // class is concrete or generic 13 | // method is concrete or generic 14 | // return is void, ref type, value type 15 | 16 | public class MockPlaceholderType { } 17 | public static class PatchedAssemblyBridge 18 | { 19 | public static bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, Type[] methodGenericTypes, object[] args) 20 | { 21 | mockedReturnValue = null; 22 | return true; 23 | } 24 | } 25 | 26 | public class SimpleClass 27 | { 28 | public int Modified; 29 | 30 | public void VoidMethodPatched(int count) 31 | { 32 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(void), out var _, Type.EmptyTypes, new object[] { count })) 33 | return; 34 | 35 | Modified += count; 36 | } 37 | 38 | public void VoidMethod(int count) 39 | { 40 | Modified += count; 41 | } 42 | 43 | public int ReturnMethodPatched(int count) 44 | { 45 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(int), out var returnValue, Type.EmptyTypes, new object[] { count })) 46 | return (int)returnValue; 47 | 48 | return Modified += count; 49 | } 50 | 51 | public int ReturnMethod(int count) 52 | { 53 | return Modified += count; 54 | } 55 | 56 | public void VoidGenericMethodPatched(T count) 57 | { 58 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(int), out var returnValue, Type.EmptyTypes, new object[] { count })) 59 | return; 60 | 61 | ++Modified; 62 | } 63 | 64 | public void VoidGenericMethod(T count) 65 | { 66 | ++Modified; 67 | } 68 | 69 | public T GenericMethodPatched(T count) 70 | { 71 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(int), out var returnValue, Type.EmptyTypes, new object[] { count })) 72 | return (T)returnValue; 73 | 74 | ++Modified; 75 | return count; 76 | } 77 | 78 | public T GenericMethod(T count) 79 | { 80 | ++Modified; 81 | return count; 82 | } 83 | } 84 | 85 | public class GenericClass 86 | { 87 | public int Modified; 88 | 89 | public void VoidMethod(int count) 90 | { 91 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(void), out var _, Type.EmptyTypes, new object[] { count })) 92 | return; 93 | 94 | Modified += count; 95 | } 96 | 97 | public int ReturnMethod(int count) 98 | { 99 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(int), out var returnValue, Type.EmptyTypes, new object[] { count })) 100 | return (int)returnValue; 101 | 102 | return Modified += count; 103 | } 104 | 105 | public void VoidGenericMethod(T count) 106 | { 107 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(int), out var returnValue, Type.EmptyTypes, new object[] { count })) 108 | return; 109 | 110 | ++Modified; 111 | } 112 | public T GenericMethod(T count) 113 | { 114 | if (PatchedAssemblyBridge.TryMock(typeof(SimpleClass), this, typeof(int), out var returnValue, Type.EmptyTypes, new object[] { count })) 115 | return (T)returnValue; 116 | 117 | ++Modified; 118 | return count; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /source/.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 2 | 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Generated files 14 | .idea/**/contentModel.xml 15 | 16 | # Sensitive or high-churn files 17 | .idea/**/dataSources/ 18 | .idea/**/dataSources.ids 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | .idea/**/dbnavigator.xml 24 | 25 | # Gradle 26 | .idea/**/gradle.xml 27 | .idea/**/libraries 28 | 29 | # Gradle and Maven with auto-import 30 | # When using Gradle or Maven with auto-import, you should exclude module files, 31 | # since they will be recreated, and may cause churn. Uncomment if using 32 | # auto-import. 33 | # .idea/modules.xml 34 | # .idea/*.iml 35 | # .idea/modules 36 | 37 | # CMake 38 | cmake-build-*/ 39 | 40 | # Mongo Explorer plugin 41 | .idea/**/mongoSettings.xml 42 | 43 | # File-based project format 44 | *.iws 45 | 46 | # IntelliJ 47 | out/ 48 | 49 | # mpeltonen/sbt-idea plugin 50 | .idea_modules/ 51 | 52 | # JIRA plugin 53 | atlassian-ide-plugin.xml 54 | 55 | # Cursive Clojure plugin 56 | .idea/replstate.xml 57 | 58 | # Crashlytics plugin (for Android Studio and IntelliJ) 59 | com_crashlytics_export_strings.xml 60 | crashlytics.properties 61 | crashlytics-build.properties 62 | fabric.properties 63 | 64 | # Editor-based Rest Client 65 | .idea/httpRequests 66 | 67 | # Android studio 3.1+ serialized cache file 68 | .idea/caches/build_file_checksums.ser 69 | -------------------------------------------------------------------------------- /source/.idea/.idea.NSubstitute.Elevated/.idea/.name: -------------------------------------------------------------------------------- 1 | NSubstitute.Elevated -------------------------------------------------------------------------------- /source/.idea/.idea.NSubstitute.Elevated/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /source/.idea/.idea.NSubstitute.Elevated/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/.idea/.idea.NSubstitute.Elevated/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/.idea/.idea.NSubstitute.Elevated/riderModule.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27019.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSubstitute.Elevated", "NSubstitute.Elevated\NSubstitute.Elevated.csproj", "{771C49B1-4768-45FA-97BA-37B56268C534}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0ADFBFB2-69DD-42A9-959C-4B476863594D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSubstitute.Elevated.Tests", "..\tests\NSubstitute.Elevated.Tests\NSubstitute.Elevated.Tests.csproj", "{F342F10E-783E-4CA6-B39A-2AE777628E3A}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36AEE027-40B5-4ECF-8A1B-4FCAE63C73B3}" 13 | ProjectSection(SolutionItems) = preProject 14 | common.targets = common.targets 15 | ..\README.md = ..\README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support", "Support", "{F19A2CFF-A4DE-4D84-8220-662EBA16283A}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemUnderTest", "..\tests\Support\SystemUnderTest\SystemUnderTest.csproj", "{4C7110B5-C596-4AE0-A67F-0AEF0E3D016D}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependentAssembly", "..\tests\Support\DependentAssembly\DependentAssembly.csproj", "{5F9C587F-9F8A-40E5-87CA-62C55481851C}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Core", "Unity.Core\Unity.Core.csproj", "{4483F618-9ADB-4A0B-A0D4-37EDB2593F06}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unity.Core.Tests", "Unity.Core.Tests\Unity.Core.Tests.csproj", "{6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {771C49B1-4768-45FA-97BA-37B56268C534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {771C49B1-4768-45FA-97BA-37B56268C534}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {771C49B1-4768-45FA-97BA-37B56268C534}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {771C49B1-4768-45FA-97BA-37B56268C534}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {F342F10E-783E-4CA6-B39A-2AE777628E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {F342F10E-783E-4CA6-B39A-2AE777628E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {F342F10E-783E-4CA6-B39A-2AE777628E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {F342F10E-783E-4CA6-B39A-2AE777628E3A}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {4C7110B5-C596-4AE0-A67F-0AEF0E3D016D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {4C7110B5-C596-4AE0-A67F-0AEF0E3D016D}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {4C7110B5-C596-4AE0-A67F-0AEF0E3D016D}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {4C7110B5-C596-4AE0-A67F-0AEF0E3D016D}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {5F9C587F-9F8A-40E5-87CA-62C55481851C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {5F9C587F-9F8A-40E5-87CA-62C55481851C}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {5F9C587F-9F8A-40E5-87CA-62C55481851C}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {5F9C587F-9F8A-40E5-87CA-62C55481851C}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {4483F618-9ADB-4A0B-A0D4-37EDB2593F06}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {6B83FD54-CA1D-4E0A-A700-71AAD1992EF1}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {F342F10E-783E-4CA6-B39A-2AE777628E3A} = {0ADFBFB2-69DD-42A9-959C-4B476863594D} 64 | {F19A2CFF-A4DE-4D84-8220-662EBA16283A} = {0ADFBFB2-69DD-42A9-959C-4B476863594D} 65 | {4C7110B5-C596-4AE0-A67F-0AEF0E3D016D} = {F19A2CFF-A4DE-4D84-8220-662EBA16283A} 66 | {5F9C587F-9F8A-40E5-87CA-62C55481851C} = {F19A2CFF-A4DE-4D84-8220-662EBA16283A} 67 | {6B83FD54-CA1D-4E0A-A700-71AAD1992EF1} = {0ADFBFB2-69DD-42A9-959C-4B476863594D} 68 | EndGlobalSection 69 | GlobalSection(ExtensibilityGlobals) = postSolution 70 | SolutionGuid = {FD843EB6-8549-43F4-A30F-53A79FF71FA7} 71 | EndGlobalSection 72 | EndGlobal 73 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("NSubstitute.Elevated.Tests")] 4 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/ElevatedSubstituteManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Mono.Cecil; 6 | using NSubstitute.Core; 7 | using NSubstitute.Elevated.WeaverInternals; 8 | using NSubstitute.Exceptions; 9 | using NSubstitute.Proxies; 10 | using NSubstitute.Proxies.CastleDynamicProxy; 11 | using NSubstitute.Proxies.DelegateProxy; 12 | using Unity.Core; 13 | using TypeAttributes = Mono.Cecil.TypeAttributes; 14 | 15 | namespace NSubstitute.Elevated 16 | { 17 | class ElevatedSubstituteManager : IProxyFactory 18 | { 19 | readonly CallFactory m_CallFactory; 20 | readonly IProxyFactory m_DefaultProxyFactory = new ProxyFactory(new DelegateProxyFactory(), new CastleDynamicProxyFactory()); 21 | readonly object[] k_MockedCtorParams = { new MockPlaceholderType() }; 22 | 23 | public ElevatedSubstituteManager(ISubstitutionContext substitutionContext) 24 | { 25 | m_CallFactory = new CallFactory(substitutionContext); 26 | } 27 | 28 | void AddMockPlaceholderToAssembly(AssemblyDefinition targetAssembly) 29 | { 30 | var mockPlaceholder = new TypeDefinition("NSubstitute.Elevated.WeaverInternals", "MockPlaceholderType", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit) 31 | { 32 | BaseType = targetAssembly.MainModule.TypeSystem.Object 33 | }; 34 | targetAssembly.MainModule.Types.Add(mockPlaceholder); 35 | } 36 | 37 | object IProxyFactory.GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments) 38 | { 39 | // TODO: 40 | // * new type MockCtorPlaceholder in elevated assy 41 | // * generate new empty ctor that takes MockCtorPlaceholder in all mocked types 42 | // * support ctor params. throw if foudn and not ForPartsOf. then ForPartsOf determines which ctor we use. 43 | // * have a note about static ctors. because they are special, and do not support disposal, can't really mock them right. 44 | // best for user to do mock/unmock of static ctors manually (i.e. move into StaticInit/StaticDispose and call directly from test code) 45 | 46 | object proxy; 47 | var substituteConfig = ElevatedSubstitutionContext.TryGetSubstituteConfig(callRouter); 48 | 49 | if (typeToProxy.IsInterface || substituteConfig == null) 50 | { 51 | proxy = m_DefaultProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments); 52 | } 53 | else if (typeToProxy == typeof(SubstituteStatic.Proxy)) 54 | { 55 | if (additionalInterfaces?.Any() == true) 56 | throw new SubstituteException("Cannot substitute interfaces as static"); 57 | if (constructorArguments.Length != 1) 58 | throw new SubstituteException("Unexpected use of SubstituteStatic.For"); 59 | 60 | // the type we want comes from SubstituteStatic.For as a single ctor arg 61 | var actualType = (Type)constructorArguments[0]; 62 | 63 | proxy = CreateStaticProxy(actualType, callRouter); 64 | } 65 | else 66 | { 67 | // requests for additional interfaces on patched types cannot be done at runtime. elevated mocking can't, 68 | // by definition, go through a runtime dynamic proxy generator that could add such things. 69 | if (additionalInterfaces.Any()) 70 | throw new SubstituteException("Cannot add interfaces at runtime to patched types"); 71 | 72 | switch (substituteConfig) { 73 | case SubstituteConfig.OverrideAllCalls: 74 | 75 | // overriding all calls includes the ctor, so it makes no sense for the user to pass in ctor args 76 | if (constructorArguments != null && constructorArguments.Any()) 77 | throw new SubstituteException("Do not pass ctor args when substituting with elevated mocks (or did you mean to use ForPartsOf?)"); 78 | 79 | // but we use a ctor arg to select the special empty ctor that we patched in 80 | constructorArguments = k_MockedCtorParams; 81 | break; 82 | case SubstituteConfig.CallBaseByDefault: 83 | var castleDynamicProxyFactory = new CastleDynamicProxyFactory(); 84 | return castleDynamicProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments); 85 | case null: 86 | break; 87 | default: 88 | throw new ArgumentOutOfRangeException(); 89 | } 90 | 91 | // var proxyWrap = Activator.CreateInstanceFrom(patchAllDependentAssemblies[1].Path, typeToProxy.FullName, false, 92 | // BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance, null, 93 | // constructorArguments, null, null); 94 | // proxy = proxyWrap.Unwrap(); 95 | 96 | proxy = Activator.CreateInstance(typeToProxy, constructorArguments); 97 | GetRouterField(proxy.GetType()).SetValue(proxy, callRouter); 98 | } 99 | 100 | return proxy; 101 | } 102 | 103 | object CreateStaticProxy(Type typeToProxy, ICallRouter callRouter) 104 | { 105 | var field = GetStaticRouterField(typeToProxy); 106 | if (field.GetValue(null) != null) 107 | throw new SubstituteException("Cannot substitute the same type twice (did you forget to Dispose() your previous substitute?)"); 108 | 109 | field.SetValue(null, callRouter); 110 | 111 | return new SubstituteStatic.Proxy(new DelegateDisposable(() => 112 | { 113 | var found = field.GetValue(null); 114 | if (found == null) 115 | throw new SubstituteException("Unexpected static unmock of an already-unmocked type"); 116 | if (found != callRouter) 117 | throw new SubstituteException("Discovered unexpected call router attached in static mock context"); 118 | 119 | field.SetValue(null, null); 120 | })); 121 | } 122 | 123 | // called from patched assembly code via the PatchedAssemblyBridge. return true if the mock is handling the behavior. 124 | // false means that the original implementation should run. 125 | public bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, MethodInfo method, Type[] methodGenericTypes, object[] args) 126 | { 127 | var field = instance == null ? GetStaticRouterField(actualType) : GetRouterField(actualType); 128 | var callRouter = (ICallRouter)field?.GetValue(instance); 129 | 130 | if (callRouter != null) 131 | { 132 | var shouldCallOriginalMethod = false; 133 | var call = m_CallFactory.Create(method, args, instance, () => shouldCallOriginalMethod = true); 134 | mockedReturnValue = callRouter.Route(call); 135 | 136 | return !shouldCallOriginalMethod; 137 | } 138 | 139 | mockedReturnValue = mockedReturnType.GetDefaultValue(); 140 | return false; 141 | } 142 | 143 | // motivation for router mapping being stored with the type/instance: 144 | // 145 | // 1. avoid problem of "gc leak vs. substitute requires disposal" by storing the router link in the instance 146 | // 2. support for struct instances (only possible to associate call routers with individual structs from the inside) 147 | // 3. is a simple way to check that a type has been patched 148 | // 149 | FieldInfo GetStaticRouterField(Type type) => m_RouterStaticFieldCache.GetOrAdd(type, t => GetRouterField(t, Weaver.MockInjector.InjectedMockStaticDataName, BindingFlags.Static)); 150 | FieldInfo GetRouterField(Type type) => m_RouterFieldCache.GetOrAdd(type, t => GetRouterField(t, Weaver.MockInjector.InjectedMockDataName, BindingFlags.Instance)); 151 | 152 | static FieldInfo GetRouterField(IReflect type, string fieldName, BindingFlags bindingFlags) 153 | { 154 | var field = type.GetField(fieldName, bindingFlags | BindingFlags.NonPublic); 155 | if (field == null) 156 | throw new SubstituteException("Cannot substitute for non-patched types"); 157 | 158 | if (field.FieldType != typeof(object)) 159 | throw new SubstituteException("Unexpected mock data type found on patched type"); 160 | 161 | return field; 162 | } 163 | 164 | readonly Dictionary m_RouterStaticFieldCache = new Dictionary(); 165 | readonly Dictionary m_RouterFieldCache = new Dictionary(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/ElevatedSubstitutionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using JetBrains.Annotations; 5 | using NiceIO; 6 | using NSubstitute.Core; 7 | using NSubstitute.Core.Arguments; 8 | using NSubstitute.Elevated.Weaver; 9 | using NSubstitute.Exceptions; 10 | using NSubstitute.Routing; 11 | using Unity.Core; 12 | 13 | namespace NSubstitute.Elevated 14 | { 15 | // motivation: 16 | // 17 | // 1. it's the clean way to hook in our own proxy factory to the nsub machinery 18 | // 2. provide access to the sub manager so patched assemblies can route hooked calls through nsub (the so-called 'elevated' mock part) 19 | // 20 | public class ElevatedSubstitutionContext : ISubstitutionContext 21 | { 22 | readonly ISubstitutionContext m_Forwarder; 23 | readonly ISubstituteFactory m_ElevatedSubstituteFactory; 24 | 25 | // ReSharper disable once MemberCanBePrivate.Global 26 | public ElevatedSubstitutionContext([NotNull] ISubstitutionContext forwarder) 27 | { 28 | m_Forwarder = forwarder; 29 | ElevatedSubstituteManager = new ElevatedSubstituteManager(this); 30 | m_ElevatedSubstituteFactory = new SubstituteFactory(this, 31 | new ElevatedCallRouterFactory(), ElevatedSubstituteManager, new CallRouterResolver()); 32 | } 33 | 34 | public static IDisposable AutoHook(string assemblyLocation) 35 | { 36 | var hookedContext = SubstitutionContext.Current; 37 | var thisContext = new ElevatedSubstitutionContext(hookedContext); 38 | SubstitutionContext.Current = thisContext; 39 | 40 | // TODO: return a new IDisposable class that also contains the list of patch results, then in caller verify that against expected (don't want to go too wide) 41 | 42 | var patchAllDependentAssemblies = ElevatedWeaver.PatchAllDependentAssemblies( 43 | new NPath(assemblyLocation), PatchOptions.PatchTestAssembly).ToList(); 44 | 45 | return new DelegateDisposable(() => 46 | { 47 | if (SubstitutionContext.Current != thisContext) 48 | throw new SubstituteException("Unexpected hook in place of ours"); 49 | SubstitutionContext.Current = hookedContext; 50 | }); 51 | } 52 | 53 | internal ElevatedSubstituteManager ElevatedSubstituteManager { get; } 54 | 55 | class ElevatedCallRouterFactory : ICallRouterFactory 56 | { 57 | public ICallRouter Create(ISubstitutionContext substitutionContext, SubstituteConfig config) 58 | => new ElevatedCallRouter(new SubstituteState(substitutionContext, config), substitutionContext, new RouteFactory()); 59 | } 60 | 61 | class ElevatedCallRouter : CallRouter 62 | { 63 | public ElevatedCallRouter(ISubstituteState substituteState, ISubstitutionContext context, IRouteFactory routeFactory) 64 | : base(substituteState, context, routeFactory) => SubstituteConfig = substituteState.SubstituteConfig; 65 | 66 | public SubstituteConfig SubstituteConfig { get; } 67 | } 68 | 69 | internal static SubstituteConfig? TryGetSubstituteConfig(ICallRouter callRouter) 70 | => (callRouter as ElevatedCallRouter)?.SubstituteConfig; 71 | 72 | // this is the only one we're overriding for now, so we can hook our own factory in there. 73 | ISubstituteFactory ISubstitutionContext.SubstituteFactory => m_ElevatedSubstituteFactory; 74 | 75 | SequenceNumberGenerator ISubstitutionContext.SequenceNumberGenerator => m_Forwarder.SequenceNumberGenerator; 76 | PendingSpecificationInfo ISubstitutionContext.PendingSpecificationInfo { get => m_Forwarder.PendingSpecificationInfo; set => m_Forwarder.PendingSpecificationInfo = value; } 77 | bool ISubstitutionContext.IsQuerying => m_Forwarder.IsQuerying; 78 | void ISubstitutionContext.AddToQuery(object target, ICallSpecification callSpecification) { m_Forwarder.AddToQuery(target, callSpecification); } 79 | void ISubstitutionContext.ClearLastCallRouter() { m_Forwarder.ClearLastCallRouter(); } 80 | IList ISubstitutionContext.DequeueAllArgumentSpecifications() { return m_Forwarder.DequeueAllArgumentSpecifications(); } 81 | void ISubstitutionContext.EnqueueArgumentSpecification(IArgumentSpecification spec) { m_Forwarder.EnqueueArgumentSpecification(spec); } 82 | ICallRouter ISubstitutionContext.GetCallRouterFor(object substitute) { return m_Forwarder.GetCallRouterFor(substitute); } 83 | IRouteFactory ISubstitutionContext.GetRouteFactory() { return m_Forwarder.GetRouteFactory(); } 84 | void ISubstitutionContext.LastCallRouter(ICallRouter callRouter) { m_Forwarder.LastCallRouter(callRouter); } 85 | ConfiguredCall ISubstitutionContext.LastCallShouldReturn(IReturn value, MatchArgs matchArgs) { return m_Forwarder.LastCallShouldReturn(value, matchArgs); } 86 | void ISubstitutionContext.RaiseEventForNextCall(Func getArguments) { m_Forwarder.RaiseEventForNextCall(getArguments); } 87 | IQueryResults ISubstitutionContext.RunQuery(Action calls) { return m_Forwarder.RunQuery(calls); } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/NSubstitute.Elevated.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(Unity_DotNetTargetFrameworks) 5 | 6 | 7 | 8 | 2018.2.1 9 | 10 | 11 | 3.1.0 12 | 13 | 14 | 0.10.1 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/PatchedAssemblyBridge.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | using NSubstitute.Core; 6 | using Unity.Core; 7 | 8 | // this namespace contains types that must be public in order to be usable from patched assemblies, yet 9 | // we do not want used from normal client api 10 | namespace NSubstitute.Elevated.WeaverInternals 11 | { 12 | // used when generating mocked default ctors 13 | public class MockPlaceholderType {} 14 | 15 | // important: keep all non-mscorlib types out of the public surface area of this class, so as to 16 | // avoid needing to add more references than NSubstitute.Elevated to the assembly during patching. 17 | 18 | public static class PatchedAssemblyBridge 19 | { 20 | // returns true if a mock is in place and it is taking over functionality. instance may be null 21 | // if static. mockedReturnValue is ignored in a void return func. 22 | [MethodImpl(MethodImplOptions.NoInlining)] 23 | public static bool TryMock(Type actualType, object instance, Type mockedReturnType, out object mockedReturnValue, Type[] methodGenericTypes, object[] args) 24 | { 25 | if (!(SubstitutionContext.Current is ElevatedSubstitutionContext elevated)) 26 | { 27 | mockedReturnValue = mockedReturnType.GetDefaultValue(); 28 | return false; 29 | } 30 | 31 | var method = (MethodInfo) new StackTrace(1).GetFrame(0).GetMethod(); 32 | 33 | if (method.IsGenericMethodDefinition) 34 | method = method.MakeGenericMethod(methodGenericTypes); 35 | 36 | return elevated.ElevatedSubstituteManager.TryMock(actualType, instance, mockedReturnType, out mockedReturnValue, method, methodGenericTypes, args); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/SubstituteStatic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NSubstitute.Elevated 4 | { 5 | public static class SubstituteStatic 6 | { 7 | // callers need an actual object in order to chain further arranging, so we return this placeholder for static substitutes 8 | // TODO: add a finalizer that throws if static proxy was not disposed by user. add a GC.WaitForPendingFinalizers to un-hook in the sub context. add comments explaining it all. 9 | public class Proxy : IDisposable 10 | { 11 | readonly IDisposable m_Forwarder; 12 | 13 | internal Proxy(IDisposable forwarder) => m_Forwarder = forwarder; 14 | public void Dispose() => m_Forwarder.Dispose(); 15 | } 16 | 17 | // best to wrap static substitutes in `using` so they will auto-dispose. this is important because we're dealing with 18 | // statics, which are global, and without cleanup, substitute will accidentally leak across tests. 19 | public static Proxy For() => For(typeof(T)); 20 | public static Proxy For(Type staticType) => Substitute.For(staticType); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/Weaver/CecilExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using JetBrains.Annotations; 5 | using Mono.Cecil; 6 | using Unity.Core; 7 | 8 | namespace NSubstitute.Elevated.Weaver 9 | { 10 | public enum IncludeNested { No, Yes } 11 | 12 | public static class CecilExtensions 13 | { 14 | [NotNull] 15 | public static IEnumerable SelectTypes([NotNull] this AssemblyDefinition @this, IncludeNested includeNested) 16 | { 17 | var types = @this.Modules.SelectMany(m => m.Types); 18 | if (includeNested == IncludeNested.Yes) 19 | types = types.SelectMany(t => t.NestedTypes.Append(t)); 20 | return types; 21 | } 22 | 23 | public static int InheritanceChainLength([NotNull] this TypeReference @this) 24 | { 25 | if (@this.DeclaringType == null) 26 | return 0; 27 | 28 | var baseType = @this.Resolve().BaseType; 29 | if (baseType == null) 30 | return 1; 31 | 32 | return 1 + InheritanceChainLength(baseType); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/Weaver/ElevatedWeaver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using JetBrains.Annotations; 8 | using Mono.Cecil; 9 | using NiceIO; 10 | using Unity.Core; 11 | 12 | namespace NSubstitute.Elevated.Weaver 13 | { 14 | [Flags] 15 | public enum PatchOptions 16 | { 17 | PatchTestAssembly = 1 << 0, // typically we don't want to patch the test assembly itself, only the systems under test 18 | SkipPeVerify = 1 << 1, // maybe flip this bit the other way when we get a really solid weaver (peverify has an obvious perf cost) 19 | } 20 | 21 | public static class ElevatedWeaver 22 | { 23 | const string k_PatchBackupExtension = ".orig"; 24 | 25 | public static string GetPatchBackupPathFor(string path) 26 | => path + k_PatchBackupExtension; 27 | 28 | public static IReadOnlyCollection PatchAllDependentAssemblies(NPath testAssemblyPath, PatchOptions patchOptions) 29 | { 30 | // TODO: ensure we do not have any assemblies that we want to patch already loaded 31 | // (this will require the separate in-memory patching ability) 32 | 33 | // this dll has types we're going to be injecting, so ensure it is in the same folder 34 | //var targetWeaverDll 35 | 36 | var toProcess = new List { testAssemblyPath.FileMustExist() }; 37 | var patchResults = new Dictionary(StringComparer.OrdinalIgnoreCase); 38 | var mockInjector = new MockInjector(); 39 | 40 | EnsureMockTypesAssemblyInFolder(testAssemblyPath.Parent); 41 | 42 | for (var toProcessIndex = 0; toProcessIndex < toProcess.Count; ++toProcessIndex) 43 | { 44 | var assemblyToPatchPath = toProcess[toProcessIndex]; 45 | 46 | // as we accumulate dependencies recursively, we will probably get some duplicates we can early-out on 47 | if (patchResults.ContainsKey(assemblyToPatchPath)) 48 | continue; 49 | 50 | using (var assemblyToPatch = AssemblyDefinition.ReadAssembly(assemblyToPatchPath)) 51 | { 52 | GatherReferences(assemblyToPatchPath, assemblyToPatch); 53 | 54 | var patchResult = TryPatch(assemblyToPatchPath, assemblyToPatch); 55 | patchResults.Add(assemblyToPatchPath, patchResult); 56 | } 57 | } 58 | 59 | void GatherReferences(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch) 60 | { 61 | foreach (var referencedAssembly in assemblyToPatch.Modules.SelectMany(m => m.AssemblyReferences)) 62 | { 63 | // only patch dll's we "own", that are in the same folder as the test assembly 64 | var referencedAssemblyPath = assemblyToPatchPath.Parent.Combine(referencedAssembly.Name + ".dll"); 65 | 66 | if (referencedAssemblyPath.FileExists()) 67 | toProcess.Add(referencedAssemblyPath); 68 | else if (!patchResults.ContainsKey(referencedAssembly.Name)) 69 | patchResults.Add(referencedAssembly.Name, new PatchResult(referencedAssembly.Name, null, PatchState.IgnoredForeignAssembly)); 70 | } 71 | } 72 | 73 | PatchResult TryPatch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch) 74 | { 75 | var alreadyPatched = MockInjector.IsPatched(assemblyToPatch); 76 | var cannotPatch = assemblyToPatch.Name.HasPublicKey; 77 | 78 | if (assemblyToPatchPath == testAssemblyPath && (patchOptions & PatchOptions.PatchTestAssembly) == 0) 79 | { 80 | if (alreadyPatched) 81 | throw new Exception("Unexpected already-patched test assembly, yet we want PatchTestAssembly.No"); 82 | if (cannotPatch) 83 | throw new Exception("Cannot patch an assembly with a strong name"); 84 | return new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredTestAssembly); 85 | } 86 | 87 | if (alreadyPatched) 88 | return new PatchResult(assemblyToPatchPath, null, PatchState.AlreadyPatched); 89 | if (cannotPatch) 90 | return new PatchResult(assemblyToPatchPath, null, PatchState.IgnoredForeignAssembly); 91 | 92 | return Patch(assemblyToPatchPath, assemblyToPatch); 93 | } 94 | 95 | PatchResult Patch(NPath assemblyToPatchPath, AssemblyDefinition assemblyToPatch) 96 | { 97 | mockInjector.Patch(assemblyToPatch); 98 | 99 | // atomic write of file with backup 100 | // TODO: skip backup if existing file already patched. want the .orig to only be the unpatched file. 101 | 102 | // write to tmp and release the lock 103 | var tmpPath = assemblyToPatchPath.ChangeExtension(".tmp"); 104 | tmpPath.DeleteIfExists(); 105 | assemblyToPatch.Write(tmpPath); // $$$ , new WriterParameters { WriteSymbols = true }); see https://github.com/jbevain/cecil/issues/421 106 | assemblyToPatch.Dispose(); 107 | 108 | if ((patchOptions & PatchOptions.SkipPeVerify) == 0) 109 | PeVerify.Verify(tmpPath); 110 | 111 | // move the actual file to backup, and move the tmp to actual 112 | var backupPath = GetPatchBackupPathFor(assemblyToPatchPath); 113 | File.Replace(tmpPath, assemblyToPatchPath, backupPath); 114 | 115 | // TODO: move pdb file too 116 | 117 | return new PatchResult(assemblyToPatchPath, backupPath, PatchState.Patched); 118 | } 119 | 120 | return patchResults.Values; 121 | } 122 | 123 | static void EnsureMockTypesAssemblyInFolder(NPath targetFolder) 124 | { 125 | // ensure that our assembly with the mock types is discoverable by putting in the same folder as the dll that is having its types 126 | // injected into it. we could mess with the assembly resolver to avoid this, but that won't solve the issue for appdomains and 127 | // other environments that we don't control, like peverify. 128 | 129 | var mockTypesSrcPath = new NPath(MockInjector.MockTypesAssembly.Location); 130 | var mockTypesDstPath = targetFolder.Combine(mockTypesSrcPath.FileName); 131 | 132 | if (mockTypesSrcPath != mockTypesDstPath) 133 | mockTypesSrcPath.Copy(mockTypesDstPath); 134 | } 135 | 136 | public static IReadOnlyCollection PatchAssemblies( 137 | [NotNull] List testAssemblyPaths) 138 | { 139 | var testAssemblyPath = testAssemblyPaths[0]; 140 | var testAssemblyFolder = Path.GetDirectoryName(testAssemblyPath); 141 | if (testAssemblyFolder.IsNullOrEmpty()) 142 | throw new Exception("Unable to find folder for test assembly"); 143 | testAssemblyFolder = Path.GetFullPath(testAssemblyFolder); 144 | 145 | // scope 146 | { 147 | var thisAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 148 | if (thisAssemblyFolder.IsNullOrEmpty()) 149 | throw new Exception("Can only patch assemblies on disk"); 150 | thisAssemblyFolder = Path.GetFullPath(thisAssemblyFolder); 151 | 152 | // keep things really simple, at least for now 153 | if (string.Compare(testAssemblyFolder, thisAssemblyFolder, StringComparison.OrdinalIgnoreCase) != 0) 154 | throw new Exception("All assemblies must be in the same folder"); 155 | } 156 | 157 | var nsubElevatedPath = Path.Combine(testAssemblyFolder, "NSubstitute.Elevated.dll"); 158 | var mockInjector = new MockInjector(); 159 | 160 | foreach (var assemblyPath in testAssemblyPaths) 161 | { 162 | var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath); 163 | mockInjector.Patch(assemblyDefinition); 164 | // atomic write of file with backup 165 | var tmpPath = assemblyPath.Split(new[] { ".dll" }, StringSplitOptions.None)[0] + ".tmp"; 166 | File.Delete(tmpPath); 167 | assemblyDefinition.Write(tmpPath);//$$$$, new WriterParameters { WriteSymbols = true }); // getting exception, haven't looked into it yet 168 | assemblyDefinition.Dispose(); 169 | /*var originalPath = GetPatchBackupPathFor(assemblyToPatchPath); 170 | File.Replace(tmpPath, assemblyToPatchPath, originalPath);*/ 171 | // $$$ TODO: move pdb file too 172 | } 173 | 174 | return null; 175 | } 176 | } 177 | 178 | public enum PatchState 179 | { 180 | GeneralFailure, // something else went wrong 181 | IgnoredTestAssembly, // don't patch the test assembly itself, as we're requiring that to always be separate from the systems under test 182 | IgnoredForeignAssembly, // don't want to patch things that are not "ours" 183 | //AlreadyPatchedOld, // assy already patched against an older set of tooling TODO: implement 184 | AlreadyPatched, // assy already patched against current tooling 185 | Patched, // assy patched and old one backed up 186 | } 187 | 188 | public struct PatchResult 189 | { 190 | public string Path; 191 | public string OriginalPath; 192 | public PatchState PatchState; 193 | 194 | [DebuggerStepThrough] 195 | public PatchResult(string path, string originalPath, PatchState patchState) 196 | { 197 | Path = path; 198 | OriginalPath = originalPath; 199 | PatchState = patchState; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/Weaver/MockInjector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Security.Policy; 6 | using Mono.Cecil; 7 | using Mono.Cecil.Cil; 8 | using Mono.Cecil.Rocks; 9 | using NSubstitute.Elevated.WeaverInternals; 10 | using Unity.Core; 11 | using Assembly = System.Reflection.Assembly; 12 | using AssemblyMetadataAttribute = System.Reflection.AssemblyMetadataAttribute; 13 | 14 | namespace NSubstitute.Elevated.Weaver 15 | { 16 | class MockInjector 17 | { 18 | static readonly string k_MarkAsPatchedKey, k_MarkAsPatchedValue; 19 | 20 | readonly TypeDefinition m_MockPlaceholderType; 21 | readonly MethodDefinition m_PatchedAssemblyBridgeTryMock; 22 | 23 | public static Assembly MockTypesAssembly { get; } = Assembly.GetExecutingAssembly(); 24 | public const string InjectedMockStaticDataName = "__mock__staticData", InjectedMockDataName = "__mock__data"; 25 | 26 | static MockInjector() 27 | { 28 | var assemblyHash = MockTypesAssembly.Evidence.GetHostEvidence(); 29 | if (assemblyHash == null) 30 | throw new Exception("Assembly not stamped with a hash"); 31 | 32 | k_MarkAsPatchedKey = MockTypesAssembly.GetName().Name; 33 | k_MarkAsPatchedValue = assemblyHash.SHA1.ToHexString(); 34 | } 35 | 36 | public MockInjector() 37 | { 38 | using (var mockTypesDefinition = AssemblyDefinition.ReadAssembly(MockTypesAssembly.Location)) 39 | { 40 | m_MockPlaceholderType = mockTypesDefinition.MainModule 41 | .GetType(typeof(MockPlaceholderType).FullName); 42 | 43 | m_PatchedAssemblyBridgeTryMock = mockTypesDefinition.MainModule 44 | .GetType(typeof(PatchedAssemblyBridge).FullName) 45 | .Methods.Single(m => m.Name == nameof(PatchedAssemblyBridge.TryMock)); 46 | } 47 | } 48 | 49 | public void Patch(AssemblyDefinition assembly) 50 | { 51 | // patch all types 52 | 53 | var typesToProcess = assembly 54 | .SelectTypes(IncludeNested.Yes) 55 | .OrderBy(t => t.InheritanceChainLength()) // process base classes first 56 | .ToList(); // copy to a list in case patch work we do would invalidate the enumerator 57 | 58 | foreach (var type in typesToProcess) 59 | Patch(type, assembly); 60 | 61 | // add an attr to mark the assembly as patched 62 | 63 | var mainModule = assembly.MainModule; 64 | var types = mainModule.TypeSystem; 65 | 66 | var metadataAttrName = typeof(AssemblyMetadataAttribute); 67 | var metadataAttrType = new TypeReference(metadataAttrName.Namespace, metadataAttrName.Name, mainModule, types.CoreLibrary); 68 | var metadataAttrCtor = new MethodReference(".ctor", types.Void, metadataAttrType) { HasThis = true }; 69 | metadataAttrCtor.Parameters.Add(new ParameterDefinition(types.String)); 70 | metadataAttrCtor.Parameters.Add(new ParameterDefinition(types.String)); 71 | 72 | var metadataAttr = new CustomAttribute(metadataAttrCtor); 73 | metadataAttr.ConstructorArguments.Add(new CustomAttributeArgument(types.String, k_MarkAsPatchedKey)); 74 | metadataAttr.ConstructorArguments.Add(new CustomAttributeArgument(types.String, k_MarkAsPatchedValue)); 75 | 76 | assembly.CustomAttributes.Add(metadataAttr); 77 | } 78 | 79 | public static bool IsPatched(AssemblyDefinition assembly) 80 | { 81 | return assembly.CustomAttributes.Any(a => 82 | a.AttributeType.FullName == typeof(AssemblyMetadataAttribute).FullName && 83 | a.ConstructorArguments.Count == 2 && 84 | a.ConstructorArguments[0].Value as string == k_MarkAsPatchedKey && 85 | a.ConstructorArguments[1].Value as string == k_MarkAsPatchedValue); 86 | } 87 | 88 | public static bool IsPatched(string assemblyPath) 89 | { 90 | using (var assembly = AssemblyDefinition.ReadAssembly(assemblyPath)) 91 | return IsPatched(assembly); 92 | } 93 | 94 | void Patch(TypeDefinition type, AssemblyDefinition assembly) 95 | { 96 | if (type.IsInterface) 97 | return; 98 | if (type.IsNestedPrivate) 99 | return; 100 | if (type.Name == "") 101 | return; 102 | if (type.BaseType.FullName == "System.MulticastDelegate") 103 | return; 104 | if (type.IsExplicitLayout) 105 | return; 106 | if (type.CustomAttributes.Any(a => a.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) 107 | return; 108 | 109 | try 110 | { 111 | foreach (var method in type.Methods) 112 | Patch(method, assembly); 113 | 114 | void AddField(string fieldName, FieldAttributes fieldAttributes) 115 | { 116 | type.Fields.Add(new FieldDefinition(fieldName, 117 | FieldAttributes.Private | fieldAttributes, 118 | type.Module.TypeSystem.Object)); 119 | } 120 | 121 | AddField(InjectedMockStaticDataName, FieldAttributes.Static); 122 | AddField(InjectedMockDataName, 0); 123 | 124 | AddMockCtor(type); 125 | } 126 | catch (Exception e) 127 | { 128 | throw new Exception($"Internal error during mock injection into type {type.FullName}", e); 129 | } 130 | } 131 | 132 | public static bool IsPatched(TypeDefinition type) 133 | { 134 | var mockStaticField = type.Fields.SingleOrDefault(f => f.Name == InjectedMockStaticDataName); 135 | var mockField = type.Fields.SingleOrDefault(f => f.Name == InjectedMockDataName); 136 | if ((mockStaticField != null) != (mockField != null)) 137 | throw new Exception("Unexpected mismatch between static and instance mock injected fields"); 138 | 139 | return mockStaticField != null; 140 | } 141 | 142 | void AddMockCtor(TypeDefinition type) 143 | { 144 | var ctor = new MethodDefinition(".ctor", 145 | MethodAttributes.Public | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName | MethodAttributes.HideBySig, 146 | type.Module.TypeSystem.Void) 147 | { 148 | IsManaged = true, 149 | DeclaringType = type, 150 | HasThis = true, 151 | }; 152 | ctor.Parameters.Add(new ParameterDefinition(type.Module.ImportReference(m_MockPlaceholderType))); 153 | 154 | var il = ctor.Body.GetILProcessor(); 155 | 156 | var baseCtors = type.BaseType.Resolve().GetConstructors().Where(c=> !c.IsStatic); 157 | 158 | var baseMockCtor = (MethodReference)baseCtors.SingleOrDefault(c => c.Parameters.SequenceEqual(ctor.Parameters)); 159 | if (baseMockCtor != null) 160 | { 161 | baseMockCtor = type.BaseType.IsGenericInstance 162 | ? new MethodReference(baseMockCtor.Name, baseMockCtor.ReturnType, type.BaseType) { HasThis = baseMockCtor.HasThis } 163 | : type.Module.ImportReference(baseMockCtor); 164 | 165 | il.Append(il.Create(OpCodes.Ldarg_0)); 166 | il.Append(il.Create(OpCodes.Ldarg_1)); 167 | il.Append(il.Create(OpCodes.Call, baseMockCtor)); 168 | } 169 | else 170 | { 171 | var baseCtor = type.Module.ImportReference(baseCtors.Single(c => !c.Parameters.Any())); 172 | 173 | il.Append(il.Create(OpCodes.Ldarg_0)); 174 | il.Append(il.Create(OpCodes.Call, baseCtor)); 175 | } 176 | 177 | il.Append(il.Create(OpCodes.Ret)); 178 | 179 | type.Methods.Add(ctor); 180 | } 181 | 182 | void Patch(MethodDefinition method, AssemblyDefinition assembly) 183 | { 184 | if (method.IsCompilerControlled || method.IsConstructor || method.IsAbstract || !method.HasBody) 185 | return; 186 | 187 | method.Body.InitLocals = true; 188 | var originalType = assembly.MainModule.ImportReference(Type.GetType("System.Type")); 189 | var getTypeFromHandle = assembly.MainModule.ImportReference(originalType.Resolve().Methods.Single(m => m.Name == "GetTypeFromHandle")); 190 | //var getTypeFromHandle = assembly.MainModule.Import(new MethodReference("GetTypeFromHandle", type, type) { Parameters = { new ParameterDefinition(runtimeTypeHandle) } }); 191 | var emptyTypes = assembly.MainModule.ImportReference(originalType.Resolve().Fields.Single(f => f.Name == "EmptyTypes")); 192 | 193 | var v1 = new VariableDefinition(assembly.MainModule.TypeSystem.Object); 194 | method.Body.Variables.Add(v1); 195 | var bodyInstructions = new List(method.Body.Instructions); 196 | method.Body.Instructions.Clear(); 197 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldtoken, method.DeclaringType)); 198 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Call, getTypeFromHandle)); 199 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); 200 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldtoken, method.ReturnType)); 201 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Call, getTypeFromHandle)); 202 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldloca_S, v1)); 203 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, emptyTypes)); 204 | 205 | // TODO: Parameter specific 206 | if (method.Parameters.Count > 0) 207 | { 208 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_1)); 209 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Newarr, assembly.MainModule.TypeSystem.Object)); 210 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Dup)); 211 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_0)); 212 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_1)); // arg 213 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Box, method.Parameters[0].ParameterType)); 214 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Stelem_Ref)); 215 | } 216 | else 217 | { 218 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_0)); 219 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Newarr, assembly.MainModule.TypeSystem.Object)); 220 | } 221 | 222 | // End of parameter include 223 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Call, assembly.MainModule.ImportReference(m_PatchedAssemblyBridgeTryMock))); 224 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Nop)); 225 | 226 | var count = method.Body.Instructions.Count; 227 | 228 | var hasReturnValue = method.ReturnType != assembly.MainModule.TypeSystem.Void; 229 | if (hasReturnValue) 230 | { 231 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldloc_S, v1)); 232 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Unbox_Any, method.ReturnType)); 233 | } 234 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); 235 | 236 | foreach (var instruction in bodyInstructions) 237 | { 238 | method.Body.Instructions.Add(instruction); 239 | } 240 | method.Body.Instructions[count - 1] = Instruction.Create(OpCodes.Brfalse_S, method.Body.Instructions[count + (hasReturnValue ? 3 : 1)]); 241 | 242 | /*method.Body.Instructions.Clear(); 243 | 244 | 245 | ConvertReturnTypeToDefault(method.ReturnType, method.Body.Instructions); 246 | method.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));*/ 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /source/NSubstitute.Elevated/Weaver/PeVerify.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Unity.Core; 4 | 5 | namespace NSubstitute.Elevated.Weaver 6 | { 7 | public class PeVerifyException : Exception 8 | { 9 | public PeVerifyException(string assemblyName, int exitCode, IEnumerable log) 10 | : base( 11 | $"Failed to PEVerify assembly '{assemblyName}' (exit={exitCode})\n\n" 12 | + log.StringJoin('\n')) { } 13 | } 14 | 15 | public static class PeVerify 16 | { 17 | // TODO: detect, make compat with dotnet core for xplat.. 18 | // others have solved it like this: 19 | // https://github.com/castleproject-deprecated/Castle.Core-READONLY/blob/master/src/Castle.Core.Tests/BasePEVerifyTestCase.cs 20 | // https://github.com/rsdn/nemerle/blob/6904f590bf8b25a97838c6733dd2e53bd68467fd/snippets/codegentests/Tests/Verification/PeVerify.cs#L55 21 | // https://github.com/bamboo/boo/blob/1f2f60a08ca69e95db34b30154985170f4723cad/src/Boo.Lang.Compiler/Steps/PEVerify.cs#L47 22 | public static string ExePath => @"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\x64\PEVerify.exe"; 23 | 24 | public static void Verify(string assemblyName) 25 | { 26 | var log = new List(); 27 | var rc = ProcessUtility.ExecuteCommandLine(ExePath, new[] { "/nologo", assemblyName }, null, log, log); 28 | 29 | // TODO: not great to just throw like this vs. returning an error structure 30 | // TODO: will it return 0 even if there are warnings? 31 | if (rc != 0) 32 | throw new PeVerifyException(assemblyName, rc, log); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/DictionaryExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using Shouldly; 5 | 6 | namespace Unity.Core.Tests 7 | { 8 | public class DictionaryExtensionsTests 9 | { 10 | [Test] 11 | public void OrEmpty_NonNullInput_ReturnsInput() 12 | { 13 | var dictionary = new Dictionary {[0] = "zero" }; 14 | 15 | dictionary.OrEmpty().ShouldBe(dictionary); 16 | } 17 | 18 | [Test] 19 | public void OrEmpty_NullInput_ReturnsEmpty() 20 | { 21 | IReadOnlyDictionary dictionary = null; 22 | 23 | // ReSharper disable once ExpressionIsAlwaysNull 24 | dictionary.OrEmpty().ShouldBeEmpty(); 25 | } 26 | 27 | [Test] 28 | public void GetValueOr_Found_ReturnsFound() 29 | { 30 | var dictionary = new Dictionary {[1] = "one" }; 31 | 32 | dictionary.GetValueOr(1).ShouldBe("one"); 33 | dictionary.GetValueOr(1, "two").ShouldBe("one"); 34 | } 35 | 36 | [Test] 37 | public void GetValueOr_NotFound_ReturnsDefault() 38 | { 39 | var dictionary = new Dictionary {["one"] = 1 }; 40 | 41 | dictionary.GetValueOr("two").ShouldBe(0); 42 | dictionary.GetValueOr("two", 2).ShouldBe(2); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/DiffUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using Shouldly; 4 | 5 | namespace Unity.Core.Tests 6 | { 7 | public class DiffUtilsTests 8 | { 9 | [Test] 10 | public void IsDiff_ValidLfDiff_ReturnsTrue() 11 | { 12 | var diffText = new[] 13 | { 14 | "--- a/cppupdatr/Refactor/MoveFile.cs", 15 | "+++ b/cppupdatr/Refactor/MoveFile.cs", 16 | "@@ -1,6 +1,7 @@", 17 | }.StringJoin('\n'); 18 | 19 | DiffUtils.IsDiff(diffText).ShouldBeTrue(); 20 | } 21 | 22 | [Test] 23 | public void IsDiff_ValidCrLfDiff_ReturnsTrue() 24 | { 25 | var diffText = new[] 26 | { 27 | "--- a/cppupdatr/Refactor/MoveFile.cs", 28 | "+++ b/cppupdatr/Refactor/MoveFile.cs", 29 | "@@ -1,6 +1,7 @@", 30 | }.StringJoin("\r\n"); 31 | 32 | DiffUtils.IsDiff(diffText).ShouldBeTrue(); 33 | } 34 | 35 | [Test] 36 | public void IsDiff_EmptyDiff_ReturnsFalse() 37 | { 38 | DiffUtils.IsDiff("").ShouldBeFalse(); 39 | } 40 | 41 | [Test] 42 | public void IsDiff_BrokenDiff_ReturnsFalse() 43 | { 44 | var diffText = new[] 45 | { 46 | "--- a/cppupdatr/Refactor/MoveFile.cs", 47 | " +++ b/cppupdatr/Refactor/MoveFile.cs", 48 | "@@ -1,6 +1,7 @@" 49 | }.StringJoin('\n'); 50 | 51 | DiffUtils.IsDiff(diffText).ShouldBeFalse(); 52 | } 53 | 54 | [Test] 55 | public void IsDiff_IncompleteDiff_ReturnsFalse() 56 | { 57 | var diffText = new[] 58 | { 59 | "--- a/cppupdatr/Refactor/MoveFile.cs", 60 | "+++ b/cppupdatr/Refactor/MoveFile.cs", 61 | }.StringJoin('\n'); 62 | 63 | DiffUtils.IsDiff(diffText).ShouldBeFalse(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/EnumUtilityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using NUnit.Framework; 4 | using Shouldly; 5 | 6 | namespace Unity.Core.Tests 7 | { 8 | public class EnumUtilityTests 9 | { 10 | // ReSharper disable UnusedMember.Global 11 | // ReSharper disable UnusedMember.Local 12 | // ReSharper disable InconsistentNaming 13 | enum SampleEnum { ValueOne = 1, AnotherValue = 2 << 4, AThirdValue = 123, FourthValue = -123 } 14 | enum NonCaseSensitiveUniqueNames { Value, VALUE, value } 15 | // ReSharper restore UnusedMember.Global 16 | // ReSharper restore UnusedMember.Local 17 | // ReSharper restore InconsistentNaming 18 | 19 | [Test] 20 | public void GetNames_MatchesFrameworkCall() 21 | { 22 | var utilNames = EnumUtility.GetNames(); 23 | var frameworkNames = Enum.GetNames(typeof(SampleEnum)); 24 | 25 | utilNames.ShouldBe(frameworkNames); 26 | } 27 | 28 | [Test] 29 | public void GetLowercaseNames_WithCaseSensitiveUniqueNames_MatchesLowercasedFrameworkCall() 30 | { 31 | var utilNames = EnumUtility.GetLowercaseNames(); 32 | var frameworkNames = Enum.GetNames(typeof(SampleEnum)).Select(n => n.ToLower()); 33 | 34 | utilNames.ShouldBe(frameworkNames); 35 | } 36 | 37 | [Test] 38 | public void GetLowercaseNames_WithNonCaseSensitiveUniqueNames_Throws() 39 | { 40 | Should 41 | .Throw(() => EnumUtility.GetLowercaseNames()) 42 | .Message.ShouldContain("Unexpected case insensitive duplicates"); 43 | } 44 | 45 | [Test] 46 | public void GetValues_MatchesFrameworkCall() 47 | { 48 | var utilValues = EnumUtility.GetValues(); 49 | var frameworkValues = (SampleEnum[])Enum.GetValues(typeof(SampleEnum)); 50 | 51 | utilValues.ShouldBe(frameworkValues); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/EnumerableExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Shouldly; 6 | 7 | namespace Unity.Core.Tests 8 | { 9 | public class EnumerableExtensionsTests 10 | { 11 | [Test] 12 | public void WhereNotNull_ItemsWithNulls_ReturnsFilteredForNull() 13 | { 14 | var dummy1 = Enumerable.Empty(); 15 | var dummy2 = new Exception(); 16 | var enumerable = new object[] { null, "abc", dummy1, dummy2, null, null, "ghi" }; 17 | 18 | enumerable.WhereNotNull().ShouldBe(new object[] { "abc", dummy1, dummy2, "ghi" }); 19 | } 20 | 21 | [Test] 22 | public void WhereNotNull_Empty_ReturnsEmpty() 23 | { 24 | var enumerable = Enumerable.Empty(); 25 | 26 | enumerable.WhereNotNull().ShouldBeEmpty(); 27 | } 28 | 29 | [Test] 30 | public void WhereNotNull_AllNulls_ReturnsEmpty() 31 | { 32 | var enumerable = new object[] { null, null, null }; 33 | 34 | enumerable.WhereNotNull().ShouldBeEmpty(); 35 | } 36 | 37 | [Test] 38 | public void OrEmpty_NonNullInput_ReturnsInput() 39 | { 40 | var enumerable = new string[0]; 41 | 42 | enumerable.OrEmpty().ShouldBe(enumerable); 43 | } 44 | 45 | [Test] 46 | public void OrEmpty_NullInput_ReturnsEmpty() 47 | { 48 | IEnumerable enumerable = null; 49 | 50 | // ReSharper disable once ExpressionIsAlwaysNull 51 | enumerable.OrEmpty().ShouldBeEmpty(); 52 | } 53 | 54 | [Test] 55 | public void ToDictionary_Tuples_ReturnsMappedDictionary() 56 | { 57 | var items = new[] { (1, "one"), (2, "two") }; 58 | var dictionary = items.ToDictionary(); 59 | 60 | dictionary[1].ShouldBe("one"); 61 | dictionary[2].ShouldBe("two"); 62 | } 63 | 64 | [Test] 65 | public void ToDictionary_TuplesWithDups_Throws() 66 | { 67 | var items = new[] { (1, "one"), (1, "two") }; 68 | Should.Throw(() => items.ToDictionary()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/ExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using NUnit.Framework; 4 | using Shouldly; 5 | 6 | // ReSharper disable AssignNullToNotNullAttribute 7 | // ReSharper disable ExpressionIsAlwaysNull 8 | 9 | namespace Unity.Core.Tests 10 | { 11 | public class RefTypeExtensionsTests 12 | { 13 | [Test] 14 | public void WrapEnumerables_NonNullInput_ReturnsInputWrappedInEnumerable() 15 | { 16 | const string item = "test"; 17 | 18 | var enumerable = item.WrapInEnumerable(); 19 | enumerable.ShouldBe(new[] { item }); 20 | 21 | enumerable = item.WrapInEnumerableOrEmpty(); 22 | enumerable.ShouldBe(new[] { item }); 23 | } 24 | 25 | [Test] 26 | public void WrapEnumerable_NullInput_ReturnsNullWrappedInEnumerable() 27 | { 28 | string item = null; 29 | var enumerable = item.WrapInEnumerable(); 30 | enumerable.ShouldBe(new[] { item }); 31 | } 32 | 33 | [Test] 34 | public void WrapEnumerableOrEmpty_NullInput_ReturnsEmptyEnumerable() 35 | { 36 | string item = null; 37 | var enumerable = item.WrapInEnumerableOrEmpty(); 38 | enumerable.ShouldBe(Enumerable.Empty()); 39 | } 40 | } 41 | 42 | public class ComparableExtensionsTests 43 | { 44 | [Test] 45 | public void Clamp_BadRange_ShouldThrow() 46 | { 47 | Should.Throw(() => 1.Clamp (2, 1)); 48 | Should.Throw(() => 'a'.Clamp('z', 'y')); 49 | } 50 | 51 | [Test] 52 | public void Clamp_InBounds_ReturnsValue() 53 | { 54 | 5.Clamp (2, 10).ShouldBe(5); 55 | 3.14.Clamp(3, 6).ShouldBe(3.14); 56 | 'b'.Clamp('a', 'z').ShouldBe('b'); 57 | "abc".Clamp("a", "b").ShouldBe("abc"); 58 | } 59 | 60 | [Test] 61 | public void Clamp_OutOfBounds_ReturnsClampedValue() 62 | { 63 | 15.Clamp (3, 12).ShouldBe(12); 64 | (-5).Clamp(-2, 4).ShouldBe(-2); 65 | 66 | 3.14.Clamp(3.2, 4.3).ShouldBe(3.2); 67 | (-3.24).Clamp(-2.1, 1.5).ShouldBe(-2.1); 68 | 69 | 'b'.Clamp('d', 'z').ShouldBe('d'); 70 | 'f'.Clamp('a', 'c').ShouldBe('c'); 71 | 72 | "abc".Clamp("bde", "cde").ShouldBe("bde"); 73 | "hi".Clamp("abc", "foo").ShouldBe("foo"); 74 | } 75 | 76 | [Test] 77 | public void Clamp_Integer_ReturnsInclusiveClampedValue() 78 | { 79 | 5.Clamp (0, 5).ShouldBe(5); 80 | 5.Clamp (0, 5).ShouldNotBe(4); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using NUnit.Framework; 4 | using Shouldly; 5 | 6 | namespace Unity.Core.Tests 7 | { 8 | public class StringExtensionsTests 9 | { 10 | [Test] 11 | public void Left_InBounds_ReturnsSubstring() 12 | { 13 | "".Left(0).ShouldBe(""); 14 | "abc".Left(2).ShouldBe("ab"); 15 | "abc".Left(0).ShouldBe(""); 16 | } 17 | 18 | [Test] 19 | public void Left_OutOfBounds_ClampsProperly() 20 | { 21 | "".Left(10).ShouldBe(""); 22 | "abc".Left(10).ShouldBe("abc"); 23 | } 24 | 25 | [Test] 26 | public void Left_BadInput_Throws() 27 | { 28 | // ReSharper disable once AssignNullToNotNullAttribute 29 | Should.Throw(() => ((string)null).Left(1)); 30 | Should.Throw(() => "abc".Left(-1)); 31 | } 32 | 33 | [Test] 34 | public void Mid_InBounds_ReturnsSubstring() 35 | { 36 | "".Mid(0, 0).ShouldBe(""); 37 | "abc".Mid(0, 3).ShouldBe("abc"); 38 | "abc".Mid(0).ShouldBe("abc"); 39 | "abc".Mid(0, -2).ShouldBe("abc"); 40 | "abc".Mid(1, 1).ShouldBe("b"); 41 | "abc".Mid(3, 0).ShouldBe(""); 42 | "abc".Mid(0, 0).ShouldBe(""); 43 | } 44 | 45 | [Test] 46 | public void Mid_OutOfBounds_ClampsProperly() 47 | { 48 | "".Mid(10, 5).ShouldBe(""); 49 | "abc".Mid(0, 10).ShouldBe("abc"); 50 | "abc".Mid(1, 10).ShouldBe("bc"); 51 | "abc".Mid(10, 5).ShouldBe(""); 52 | } 53 | 54 | [Test] 55 | public void Mid_BadInput_Throws() 56 | { 57 | // ReSharper disable once AssignNullToNotNullAttribute 58 | Should.Throw(() => ((string)null).Mid(1, 2)); 59 | Should.Throw(() => "abc".Mid(-1)); 60 | } 61 | 62 | [Test] 63 | public void Right_InBounds_ReturnsSubstring() 64 | { 65 | "".Right(0).ShouldBe(""); 66 | "abc".Right(2).ShouldBe("bc"); 67 | "abc".Right(0).ShouldBe(""); 68 | } 69 | 70 | [Test] 71 | public void Right_OutOfBounds_ClampsProperly() 72 | { 73 | "".Right(10).ShouldBe(""); 74 | "abc".Right(10).ShouldBe("abc"); 75 | } 76 | 77 | [Test] 78 | public void Right_BadInput_Throws() 79 | { 80 | // ReSharper disable once AssignNullToNotNullAttribute 81 | Should.Throw(() => ((string)null).Right(1)); 82 | Should.Throw(() => "abc".Right(-1)); 83 | } 84 | 85 | [Test] 86 | public void Truncate_ThatDoesNotShorten_ReturnsSameInstance() 87 | { 88 | const string text = "abc def"; 89 | ReferenceEquals(text, text.Truncate(100)).ShouldBeTrue(); 90 | ReferenceEquals(text, text.Truncate(10)).ShouldBeTrue(); 91 | ReferenceEquals(text, text.Truncate(text.Length)).ShouldBeTrue(); 92 | } 93 | 94 | [Test] 95 | public void Truncate_ThatShortens_TruncatesAndAddsTrailer() 96 | { 97 | "abc def".Truncate(6).ShouldBe("abc..."); 98 | "abc def".Truncate(5).ShouldBe("ab..."); 99 | "abc def".Truncate(4).ShouldBe("a..."); 100 | "abc def".Truncate(3).ShouldBe("..."); 101 | 102 | "abc def".Truncate(6, "ghi").ShouldBe("abcghi"); 103 | "abc def".Truncate(5, "ghi").ShouldBe("abghi"); 104 | "abc def".Truncate(4, "ghi").ShouldBe("aghi"); 105 | "abc def".Truncate(3, "ghi").ShouldBe("ghi"); 106 | } 107 | 108 | [Test] 109 | public void Truncate_WithTooBigTrailer_ShouldThrow() 110 | { 111 | Should.Throw(() => "abc def".Truncate(2)); 112 | Should.Throw(() => "abc def".Truncate(5, "123456")); 113 | } 114 | 115 | [Test] 116 | public void Truncate_WithUnderflow_ShouldThrow() 117 | { 118 | Should.Throw(() => "abc def".Truncate(-2)); 119 | Should.Throw(() => "abc def".Truncate(0, "ghi")); 120 | } 121 | 122 | [Test] 123 | public void StringJoin_WithEmpty_ReturnsEmptyString() 124 | { 125 | var enumerable = new object[0]; 126 | 127 | enumerable.StringJoin(", ").ShouldBe(""); 128 | enumerable.StringJoin(';').ShouldBe(""); 129 | enumerable.StringJoin(o => o, ", ").ShouldBe(""); 130 | enumerable.StringJoin(o => o, ';').ShouldBe(""); 131 | } 132 | 133 | [Test] 134 | public void StringJoin_WithSingle_ReturnsNoSeparators() 135 | { 136 | var enumerable = new[] { "abc" }; 137 | 138 | enumerable.StringJoin(", ").ShouldBe("abc"); 139 | enumerable.StringJoin(';').ShouldBe("abc"); 140 | enumerable.StringJoin(o => o, ", ").ShouldBe("abc"); 141 | enumerable.StringJoin(o => o, ';').ShouldBe("abc"); 142 | } 143 | 144 | [Test] 145 | public void StringJoin_WithMultiple_ReturnsJoined() 146 | { 147 | var enumerable = new object[] { "abc", 0b111001, -14, 'z' }; 148 | 149 | enumerable.StringJoin(" ==> ").ShouldBe("abc ==> 57 ==> -14 ==> z"); 150 | enumerable.StringJoin('\n').ShouldBe("abc\n57\n-14\nz"); 151 | enumerable.StringJoin(o => o, " <> ").ShouldBe("abc <> 57 <> -14 <> z"); 152 | enumerable.StringJoin(o => o, ';').ShouldBe("abc;57;-14;z"); 153 | } 154 | 155 | [Test] 156 | public void StringJoin_WithSelectorAndSimpleEnumerable_ReturnsSelectedJoined() 157 | { 158 | var enumerable = new[] { "hi", "there", "this", "", "is", "some", "stuff" }; 159 | 160 | int Selector(string value) { return value.Length; } 161 | 162 | enumerable.StringJoin(Selector, ", ").ShouldBe("2, 5, 4, 0, 2, 4, 5"); 163 | enumerable.StringJoin(Selector, ';').ShouldBe("2;5;4;0;2;4;5"); 164 | } 165 | 166 | [Test] 167 | public void StringJoin_WithSelectorAndComplexEnumerable_ReturnsSelectedJoined() 168 | { 169 | var enumerable = new object[] { "abc", 123, null, ("hi", 1.23) }; 170 | 171 | string Selector(object value) { return value?.GetType().Name ?? "(null)"; } 172 | 173 | enumerable.StringJoin(Selector, " ** ").ShouldBe("String ** Int32 ** (null) ** ValueTuple`2"); 174 | enumerable.StringJoin(Selector, '?').ShouldBe("String?Int32?(null)?ValueTuple`2"); 175 | } 176 | 177 | [Test] 178 | public void ExpandTabs_WithEmpty_Returns_Empty() 179 | { 180 | "".ExpandTabs(4).ShouldBeEmpty(); 181 | } 182 | 183 | [Test] 184 | public void ExpandTabs_WithNoTabs_ReturnsSameInstance() // i.e. no allocs 185 | { 186 | const string text = "abc def ghijkl"; 187 | ReferenceEquals(text, text.ExpandTabs(4)).ShouldBeTrue(); 188 | } 189 | 190 | [Test] 191 | public void ExpandTabs_WithInvalidTabWidth_Throws() 192 | { 193 | Should.Throw(() => "".ExpandTabs(-123)); 194 | Should.Throw(() => "abc".ExpandTabs(-1)); 195 | } 196 | 197 | [Test] 198 | public void ExpandTabs_BasicScenarios_ExpandsProperly() 199 | { 200 | "a\tbc\t\td".ExpandTabs(4).ShouldBe("a bc d"); 201 | "a\tbc\t\td".ExpandTabs(3).ShouldBe("a bc d"); 202 | "a\tbc\t\td".ExpandTabs(2).ShouldBe("a bc d"); 203 | "a\tbc\t\td".ExpandTabs(1).ShouldBe("a bc d"); 204 | "a\tbc\t\td".ExpandTabs(0).ShouldBe("abcd"); 205 | } 206 | 207 | [Test] 208 | public void ExpandTabs_WithUnnecessaryBuffer_DoesNotUseBuffer() 209 | { 210 | var buffer = new StringBuilder { Capacity = 0 }; 211 | 212 | var expandeda = "\ta".ExpandTabs(0, buffer); 213 | expandeda.ShouldBe("a"); 214 | buffer.Capacity.ShouldBe(0); 215 | 216 | var expandedb = "\tb".ExpandTabs(1, buffer); 217 | expandedb.ShouldBe(" b"); 218 | buffer.Capacity.ShouldBe(0); 219 | 220 | var expandedc = "\tc".ExpandTabs(2, buffer); 221 | expandedc.ShouldBe(" c"); 222 | buffer.Capacity.ShouldNotBe(0); 223 | } 224 | 225 | [Test] 226 | public void ExpandTabs_WithReusedBuffer_DoesNotReusePreviousResults() 227 | { 228 | // this is a bugfix test. note tab width of 2 to avoid early-out that doesn't use the string builder. 229 | 230 | var buffer = new StringBuilder(); 231 | "\ta".ExpandTabs(2, buffer).ShouldBe(" a"); 232 | buffer.Length.ShouldBe(0); // no leftovers 233 | "\tb".ExpandTabs(2, buffer).ShouldBe(" b"); // not " a b" 234 | buffer.Length.ShouldBe(0); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /source/Unity.Core.Tests/Unity.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(Unity_DotNetTargetFrameworks) 5 | 6 | 7 | 8 | 2018.2.1 9 | 10 | 11 | 15.9.0 12 | 13 | 14 | 3.11.0 15 | 16 | 17 | 3.11.0 18 | 19 | 20 | 3.0.1 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /source/Unity.Core/ConsoleUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Unity.Core 5 | { 6 | public static class Stdin 7 | { 8 | public static IEnumerable SelectLines() 9 | { 10 | for (;;) 11 | { 12 | var line = Console.ReadLine(); 13 | if (line == null) 14 | yield break; 15 | 16 | yield return line; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /source/Unity.Core/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using JetBrains.Annotations; 6 | 7 | namespace Unity.Core 8 | { 9 | public class ReadOnlyDictionary 10 | { 11 | class EmptyDictionary : IReadOnlyDictionary 12 | { 13 | public static readonly IReadOnlyDictionary instance = new EmptyDictionary(); 14 | 15 | public IEnumerator> GetEnumerator() => Enumerable.Empty>().GetEnumerator(); 16 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 17 | public int Count => 0; 18 | public bool ContainsKey(TKey key) => false; 19 | public bool TryGetValue(TKey key, out TValue value) { value = default(TValue); return false; } 20 | public TValue this[TKey key] => throw new KeyNotFoundException(); 21 | public IEnumerable Keys => Enumerable.Empty(); 22 | public IEnumerable Values => Enumerable.Empty(); 23 | } 24 | 25 | public static IReadOnlyDictionary Empty() 26 | => EmptyDictionary.instance; 27 | } 28 | 29 | public static class Dictionary 30 | { 31 | [NotNull] 32 | public static Dictionary Create(params(TKey key, TValue value)[] items) 33 | => items.ToDictionary(); 34 | } 35 | 36 | public static class DictionaryExtensions 37 | { 38 | [NotNull] 39 | public static IReadOnlyDictionary OrEmpty([CanBeNull] this IReadOnlyDictionary @this) 40 | => @this ?? ReadOnlyDictionary.Empty(); 41 | 42 | public static TValue GetValueOr([NotNull] this IReadOnlyDictionary @this, TKey key, TValue defaultValue = default(TValue)) 43 | => @this.TryGetValue(key, out var value) ? value : defaultValue; 44 | 45 | public static TValue GetOrAdd([NotNull] this IDictionary @this, TKey key, [NotNull] Func createFunc) 46 | { 47 | if (@this.TryGetValue(key, out var found)) 48 | return found; 49 | 50 | found = createFunc(key); 51 | @this.Add(key, found); 52 | return found; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /source/Unity.Core/DiffUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Unity.Core 4 | { 5 | public static class DiffUtils 6 | { 7 | public static bool IsDiff(string candidate) 8 | { 9 | const string detectDiffPattern = @"(?mx) 10 | ^ 11 | ---\ [^\n]+\n 12 | \+\+\+\ [^\n]+\n 13 | @@\ "; 14 | 15 | return Regex.IsMatch(candidate, detectDiffPattern); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/Unity.Core/DisposableUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | 4 | namespace Unity.Core 5 | { 6 | public class DelegateDisposable : IDisposable 7 | { 8 | readonly Action m_DisposeAction; 9 | 10 | public DelegateDisposable([NotNull] Action disposeAction) => m_DisposeAction = disposeAction; 11 | public void Dispose() => m_DisposeAction(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/Unity.Core/EnumUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Unity.Core 6 | { 7 | public static class EnumUtility 8 | { 9 | // about `where T : struct, IComparable, IConvertible, IFormattable` 10 | // 11 | // this is how we try to detect enums, since c# doesn't let us `where T : Enum`. works ok, not perfect. 12 | 13 | public static IReadOnlyList GetNames() 14 | where T : struct, IComparable, IConvertible, IFormattable 15 | => s_NameCache.GetValueOr(typeof(T)) ?? (s_NameCache[typeof(T)] = Enum.GetNames(typeof(T))); 16 | 17 | public static IReadOnlyList GetLowercaseNames() 18 | where T : struct, IComparable, IConvertible, IFormattable 19 | => GetOrAddLowerNameCache(typeof(T)); 20 | 21 | public static IReadOnlyList GetValues() 22 | where T : struct, IComparable, IConvertible, IFormattable 23 | { 24 | var found = s_ValueCache.GetValueOr(typeof(T)) ?? (s_ValueCache[typeof(T)] = Enum.GetValues(typeof(T))); 25 | return (IReadOnlyList)found; 26 | } 27 | 28 | public static T TryParseNoCase(string enumName, T defaultValue = default(T)) 29 | where T : struct, IComparable, IConvertible, IFormattable 30 | { 31 | T value; 32 | return Enum.TryParse(enumName, true, out value) ? value : defaultValue; 33 | } 34 | 35 | public static T TryParse(string enumName, T defaultValue = default(T)) 36 | where T : struct, IComparable, IConvertible, IFormattable 37 | { 38 | T value; 39 | return Enum.TryParse(enumName, false, out value) ? value : defaultValue; 40 | } 41 | 42 | public static T? TryParseNoCaseOr(string enumName) 43 | where T : struct, IComparable, IConvertible, IFormattable 44 | { 45 | T value; 46 | return Enum.TryParse(enumName, true, out value) ? (T?)value : null; 47 | } 48 | 49 | public static T? TryParseOr(string enumName) 50 | where T : struct, IComparable, IConvertible, IFormattable 51 | { 52 | T value; 53 | return Enum.TryParse(enumName, false, out value) ? (T?)value : null; 54 | } 55 | 56 | static IReadOnlyList GetNameCache(Type enumType) 57 | { 58 | return s_NameCache.GetOrAdd(enumType, Enum.GetNames); 59 | } 60 | 61 | static IReadOnlyList GetOrAddLowerNameCache(Type enumType) 62 | { 63 | return s_LowercaseNameCache.GetOrAdd(enumType, t => 64 | { 65 | var names = GetNameCache(t); 66 | var lowerNames = names.ToLower().Distinct().ToArray(); 67 | 68 | // EnumUtility doesn't care about this, but it is probably a sign of a bug elsewhere 69 | if (names.Count != lowerNames.Length) 70 | throw new Exception($"Unexpected case insensitive duplicates found in enum {enumType.FullName}"); 71 | 72 | return lowerNames; 73 | }); 74 | } 75 | 76 | static Dictionary> s_NameCache = new Dictionary>(); 77 | static Dictionary> s_LowercaseNameCache = new Dictionary>(); 78 | static Dictionary s_ValueCache = new Dictionary(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/Unity.Core/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using JetBrains.Annotations; 4 | using Enumerable = System.Linq.Enumerable; 5 | 6 | namespace Unity.Core 7 | { 8 | public static class EnumerableExtensions 9 | { 10 | // should instead be an IReadOnlyList (jetbrains apparently uses this type to detect "safe to double-walk", 11 | // but our framework doesn't have that type yet. so we have to hack it with ICollection, which is unfortunately 12 | // read-write. extra bad because we're "unwrapping" what should be a safe enumerable. compromises.. :( 13 | [NotNull] 14 | public static ICollection UnDefer([NotNull] this IEnumerable @this) 15 | => @this as ICollection ?? @this.ToList(); // don't use ToArray, it does extra work 16 | 17 | [NotNull] 18 | public static IEnumerable WhereNotNull([NotNull] this IEnumerable @this) where T : class 19 | => @this.Where(item => !ReferenceEquals(item, null)); 20 | 21 | [NotNull] 22 | public static IEnumerable OrEmpty([CanBeNull] this IEnumerable @this) 23 | => @this ?? Enumerable.Empty(); 24 | 25 | [NotNull] 26 | public static HashSet ToHashSet([NotNull] this IEnumerable @this, IEqualityComparer comparer) 27 | => new HashSet(@this, comparer); 28 | 29 | [NotNull] 30 | public static HashSet ToHashSet([NotNull] this IEnumerable @this) 31 | => new HashSet(@this); 32 | 33 | [NotNull] 34 | public static Dictionary ToDictionary([NotNull] this IEnumerable<(TKey key, TValue value)> @this) 35 | => @this.ToDictionary(item => item.key, item => item.value); 36 | 37 | public static IEnumerable Append([NotNull] this IEnumerable @this, T value) 38 | { 39 | foreach (var i in @this) 40 | yield return i; 41 | yield return value; 42 | } 43 | 44 | public static IEnumerable Prepend([NotNull] this IEnumerable @this, T value) 45 | { 46 | yield return value; 47 | foreach (var i in @this) 48 | yield return i; 49 | } 50 | 51 | public static bool IsNullOrEmpty([CanBeNull] this IEnumerable @this) 52 | => @this == null || !@this.Any(); 53 | 54 | public static IEnumerable SelectMany([NotNull] this IEnumerable> @this) 55 | => @this.SelectMany(_ => _); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /source/Unity.Core/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using JetBrains.Annotations; 7 | 8 | namespace Unity.Core 9 | { 10 | public static class LegacyExtensions 11 | { 12 | public static IEnumerable FixLegacy([NotNull] this MatchCollection @this) 13 | => @this.Cast(); 14 | } 15 | 16 | public static class ObjectExtensions 17 | { 18 | // fluent operators - note that we're limiting to ref types where needed to avoid accidental boxing 19 | 20 | public static T ToBase(this T @this) => @this; // sometimes you need an inline upcast 21 | 22 | public static T To(this object @this) where T : class => (T)@this; 23 | public static T As(this object @this) where T : class => @this as T; 24 | public static bool Is(this object @this) where T : class => @this is T; 25 | public static bool IsNot(this object @this) where T : class => !(@this is T); 26 | } 27 | 28 | public static class RefTypeExtensions 29 | { 30 | [NotNull] 31 | public static IEnumerable WrapInEnumerable(this T @this) 32 | { yield return @this; } 33 | 34 | [NotNull] 35 | public static IEnumerable WrapInEnumerableOrEmpty([CanBeNull] this T @this) where T : class 36 | => ReferenceEquals(@this, null) ? Enumerable.Empty() : WrapInEnumerable(@this); 37 | } 38 | 39 | public static class TypeExtensions 40 | { 41 | public static object GetDefaultValue([NotNull] this Type @this) 42 | { 43 | object defaultValue = null; 44 | if (@this.IsValueType && @this != typeof(void)) 45 | defaultValue = Activator.CreateInstance(@this); 46 | return defaultValue; 47 | } 48 | } 49 | 50 | public static class ComparableExtensions 51 | { 52 | public static T Clamp([NotNull] this T @this, T min, T max) where T : IComparable 53 | { 54 | if (min.CompareTo(max) > 0) 55 | throw new ArgumentException("'min' cannot be greater than 'max'", nameof(min)); 56 | 57 | if (@this.CompareTo(min) < 0) return min; 58 | if (@this.CompareTo(max) > 0) return max; 59 | return @this; 60 | } 61 | } 62 | 63 | public static class ByteArrayExtensions 64 | { 65 | // if you want to speed this up, see https://stackoverflow.com/q/311165/14582 66 | public static string ToHexString([NotNull] this byte[] @this) 67 | => BitConverter.ToString(@this).Replace("-", ""); 68 | } 69 | 70 | public static class ListExtensions 71 | { 72 | public static void SetRange([NotNull] this List @this, IEnumerable collection) 73 | { 74 | @this.Clear(); 75 | @this.AddRange(collection); 76 | } 77 | 78 | public static T PopBack([NotNull] this IList @this) 79 | { 80 | var item = @this[@this.Count - 1]; 81 | @this.DropBack(); 82 | return item; 83 | } 84 | 85 | public static void DropBack([NotNull] this IList @this) 86 | { 87 | @this.RemoveAt(@this.Count - 1); 88 | } 89 | } 90 | 91 | public static class StringCollectionExtensions 92 | { 93 | public static void AddRange([NotNull] this StringCollection @this, IEnumerable collection) 94 | { 95 | foreach (var item in collection) 96 | @this.Add(item); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /source/Unity.Core/HasParent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if WIP 4 | 5 | // the intention here is to provide something similar to ITreeEnumerable, part of an 6 | // ability to write abstract graph algorithms and then easily map onto types with 7 | // existing parent structures. 8 | 9 | namespace Unity.Core 10 | { 11 | public interface IHasParent 12 | { 13 | T Parent { get; } 14 | } 15 | 16 | public class HasParentDelegate : IHasParent 17 | { 18 | Func m_ParentGetter; 19 | 20 | public HasParentDelegate(T @this, Func parentGetter) 21 | { 22 | This = @this; 23 | m_ParentGetter = parentGetter; 24 | } 25 | 26 | public T Parent => m_ParentGetter(This); 27 | public T This { get; } 28 | } 29 | 30 | public static class HasParentDelegate 31 | { 32 | public static HasParentDelegate Create(T @this, Func parentGetter) 33 | => new HasParentDelegate(@this, parentGetter); 34 | } 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /source/Unity.Core/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Unity.Core 6 | { 7 | public interface ILogger 8 | { 9 | void Error(string message); 10 | void Info(string message); 11 | void Debug(string message); 12 | } 13 | 14 | public class DefaultLogger : ILogger 15 | { 16 | static DefaultLogger s_Instance = new DefaultLogger(); 17 | 18 | public static ILogger Instance => s_Instance; 19 | 20 | void ILogger.Error(string message) => Console.Error.WriteLine(message); 21 | void ILogger.Info(string message) => Console.WriteLine(message); 22 | void ILogger.Debug(string message) => Debug.WriteLine(message); 23 | } 24 | 25 | public class StringLogger : ILogger 26 | { 27 | List m_Errors = new List(); 28 | List m_Infos = new List(); 29 | List m_Debugs = new List(); 30 | 31 | void ILogger.Error(string message) => m_Errors.Add(message); 32 | void ILogger.Info(string message) => m_Infos.Add(message); 33 | void ILogger.Debug(string message) => m_Debugs.Add(message); 34 | 35 | public IReadOnlyCollection Errors => m_Errors; 36 | public IReadOnlyCollection Infos => m_Infos; 37 | public IReadOnlyCollection Debugs => m_Debugs; 38 | 39 | public string ErrorsAsString => m_Errors.StringJoin('\n'); 40 | public string InfosAsString => m_Infos.StringJoin('\n'); 41 | public string DebugsAsString => m_Debugs.StringJoin('\n'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/Unity.Core/NiceIO/NiceIO.cs: -------------------------------------------------------------------------------- 1 | // The MIT License(MIT) 2 | // ===================== 3 | // 4 | // Copyright © `2015-2017` `Lucas Meijer` 5 | // 6 | // Permission is hereby granted, free of charge, to any person 7 | // obtaining a copy of this software and associated documentation 8 | // files (the “Software”), to deal in the Software without 9 | // restriction, including without limitation the rights to use, 10 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the 12 | // Software is furnished to do so, subject to the following 13 | // conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | // OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | using System; 28 | using System.Collections.Generic; 29 | using System.Diagnostics; 30 | using System.IO; 31 | using System.Linq; 32 | using System.Text; 33 | 34 | namespace NiceIO 35 | { 36 | public class NPath : IEquatable, IComparable 37 | { 38 | private static readonly StringComparison PathStringComparison = IsLinux() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; 39 | 40 | private readonly string[] _elements; 41 | private readonly bool _isRelative; 42 | private readonly string _driveLetter; 43 | 44 | #region construction 45 | 46 | public NPath(string path) 47 | { 48 | if (path == null) 49 | throw new ArgumentNullException(); 50 | 51 | path = ParseDriveLetter(path, out _driveLetter); 52 | 53 | if (path == "/") 54 | { 55 | _isRelative = false; 56 | _elements = new string[] {}; 57 | } 58 | else 59 | { 60 | var split = path.Split('/', '\\'); 61 | 62 | _isRelative = _driveLetter == null && IsRelativeFromSplitString(split); 63 | 64 | _elements = ParseSplitStringIntoElements(split.Where(s => s.Length > 0).ToArray()); 65 | } 66 | } 67 | 68 | private NPath(string[] elements, bool isRelative, string driveLetter) 69 | { 70 | _elements = elements; 71 | _isRelative = isRelative; 72 | _driveLetter = driveLetter; 73 | } 74 | 75 | private string[] ParseSplitStringIntoElements(IEnumerable inputs) 76 | { 77 | var stack = new List(); 78 | 79 | foreach (var input in inputs.Where(input => input.Length != 0)) 80 | { 81 | if (input == ".") 82 | { 83 | if ((stack.Count > 0) && (stack.Last() != ".")) 84 | continue; 85 | } 86 | else if (input == "..") 87 | { 88 | if (HasNonDotDotLastElement(stack)) 89 | { 90 | stack.RemoveAt(stack.Count - 1); 91 | continue; 92 | } 93 | if (!_isRelative) 94 | throw new ArgumentException("You cannot create a path that tries to .. past the root"); 95 | } 96 | stack.Add(input); 97 | } 98 | return stack.ToArray(); 99 | } 100 | 101 | private static bool HasNonDotDotLastElement(List stack) 102 | { 103 | return stack.Count > 0 && stack[stack.Count - 1] != ".."; 104 | } 105 | 106 | private string ParseDriveLetter(string path, out string driveLetter) 107 | { 108 | if (path.Length >= 2 && path[1] == ':') 109 | { 110 | driveLetter = path[0].ToString(); 111 | return path.Substring(2); 112 | } 113 | 114 | driveLetter = null; 115 | return path; 116 | } 117 | 118 | private static bool IsRelativeFromSplitString(string[] split) 119 | { 120 | if (split.Length < 2) 121 | return true; 122 | 123 | return split[0].Length != 0 || !split.Any(s => s.Length > 0); 124 | } 125 | 126 | public NPath Combine(params string[] append) 127 | { 128 | return Combine(append.Select(a => new NPath(a)).ToArray()); 129 | } 130 | 131 | public NPath Combine(params NPath[] append) 132 | { 133 | var absPaths = append.Where(p => !p.IsRelative); 134 | if (absPaths.Any()) 135 | { 136 | throw new ArgumentException(string.Format( 137 | "You cannot .Combine with non-relative path(s) ({0})", 138 | string.Join(", ", absPaths.Select(p => '\'' + p + '\'').ToArray()))); 139 | } 140 | 141 | return new NPath(ParseSplitStringIntoElements(_elements.Concat(append.SelectMany(p => p._elements))), _isRelative, _driveLetter); 142 | } 143 | 144 | public NPath Parent 145 | { 146 | get 147 | { 148 | if (_elements.Length == 0) 149 | throw new InvalidOperationException("Parent is called on an empty path"); 150 | 151 | var newElements = _elements.Take(_elements.Length - 1).ToArray(); 152 | 153 | return new NPath(newElements, _isRelative, _driveLetter); 154 | } 155 | } 156 | 157 | public NPath RelativeTo(NPath path) 158 | { 159 | if (!IsChildOf(path)) 160 | { 161 | if (!IsRelative && !path.IsRelative && _driveLetter != path._driveLetter) 162 | throw new ArgumentException("Path.RelativeTo() was invoked with two paths that are on different volumes. invoked on: " + ToString() + " asked to be made relative to: " + path); 163 | 164 | NPath commonParent = null; 165 | foreach (var parent in RecursiveParents) 166 | { 167 | commonParent = path.RecursiveParents.FirstOrDefault(otherParent => otherParent == parent); 168 | 169 | if (commonParent != null) 170 | break; 171 | } 172 | 173 | if (commonParent == null) 174 | throw new ArgumentException("Path.RelativeTo() was unable to find a common parent between " + ToString() + " and " + path); 175 | 176 | if (IsRelative && path.IsRelative && commonParent.IsEmpty()) 177 | throw new ArgumentException("Path.RelativeTo() was invoked with two relative paths that do not share a common parent. Invoked on: " + ToString() + " asked to be made relative to: " + path); 178 | 179 | var depthDiff = path.Depth - commonParent.Depth; 180 | return new NPath(Enumerable.Repeat("..", depthDiff).Concat(_elements.Skip(commonParent.Depth)).ToArray(), true, null); 181 | } 182 | 183 | return new NPath(_elements.Skip(path._elements.Length).ToArray(), true, null); 184 | } 185 | 186 | public NPath ChangeExtension(string extension) 187 | { 188 | ThrowIfRoot(); 189 | 190 | var newElements = (string[])_elements.Clone(); 191 | newElements[newElements.Length - 1] = Path.ChangeExtension(_elements[_elements.Length - 1], WithDot(extension)); 192 | if (extension == string.Empty) 193 | newElements[newElements.Length - 1] = newElements[newElements.Length - 1].TrimEnd('.'); 194 | return new NPath(newElements, _isRelative, _driveLetter); 195 | } 196 | 197 | #endregion construction 198 | 199 | #region inspection 200 | 201 | public bool IsRelative 202 | { 203 | get { return _isRelative; } 204 | } 205 | 206 | public string FileName 207 | { 208 | get 209 | { 210 | ThrowIfRoot(); 211 | 212 | return _elements.Last(); 213 | } 214 | } 215 | 216 | public string FileNameWithoutExtension 217 | { 218 | get { return Path.GetFileNameWithoutExtension(FileName); } 219 | } 220 | 221 | public IEnumerable Elements 222 | { 223 | get { return _elements; } 224 | } 225 | 226 | public int Depth 227 | { 228 | get { return _elements.Length; } 229 | } 230 | 231 | public bool Exists(string append = "") 232 | { 233 | return Exists(new NPath(append)); 234 | } 235 | 236 | public bool Exists(NPath append) 237 | { 238 | return FileExists(append) || DirectoryExists(append); 239 | } 240 | 241 | public bool DirectoryExists(string append = "") 242 | { 243 | return DirectoryExists(new NPath(append)); 244 | } 245 | 246 | public bool DirectoryExists(NPath append) 247 | { 248 | return Directory.Exists(Combine(append).ToString()); 249 | } 250 | 251 | public bool FileExists(string append = "") 252 | { 253 | return FileExists(new NPath(append)); 254 | } 255 | 256 | public bool FileExists(NPath append) 257 | { 258 | return File.Exists(Combine(append).ToString()); 259 | } 260 | 261 | public string ExtensionWithDot 262 | { 263 | get 264 | { 265 | if (IsRoot) 266 | throw new ArgumentException("A root directory does not have an extension"); 267 | 268 | var last = _elements.Last(); 269 | var index = last.LastIndexOf("."); 270 | if (index < 0) return String.Empty; 271 | return last.Substring(index); 272 | } 273 | } 274 | 275 | public string ExtensionWithoutDot 276 | { 277 | get 278 | { 279 | if (IsRoot) 280 | throw new ArgumentException("A root directory does not have an extension"); 281 | 282 | var last = _elements.Last(); 283 | var index = last.LastIndexOf("."); 284 | if (index < 0) return String.Empty; 285 | return last.Substring(index + 1); 286 | } 287 | } 288 | 289 | public string InQuotes() 290 | { 291 | return "\"" + ToString() + "\""; 292 | } 293 | 294 | public string InQuotes(SlashMode slashMode) 295 | { 296 | return "\"" + ToString(slashMode) + "\""; 297 | } 298 | 299 | [DebuggerStepThrough] 300 | public override string ToString() 301 | { 302 | return ToString(SlashMode.Native); 303 | } 304 | 305 | public string ToString(SlashMode slashMode) 306 | { 307 | // Check if it's linux root / 308 | if (IsRoot && string.IsNullOrEmpty(_driveLetter)) 309 | return Slash(slashMode).ToString(); 310 | 311 | if (_isRelative && _elements.Length == 0) 312 | return "."; 313 | 314 | var sb = new StringBuilder(); 315 | if (_driveLetter != null) 316 | { 317 | sb.Append(_driveLetter); 318 | sb.Append(":"); 319 | } 320 | if (!_isRelative) 321 | sb.Append(Slash(slashMode)); 322 | var first = true; 323 | foreach (var element in _elements) 324 | { 325 | if (!first) 326 | sb.Append(Slash(slashMode)); 327 | 328 | sb.Append(element); 329 | first = false; 330 | } 331 | return sb.ToString(); 332 | } 333 | 334 | [DebuggerStepThrough] 335 | public static implicit operator string(NPath path) 336 | { 337 | return path.ToString(); 338 | } 339 | 340 | static char Slash(SlashMode slashMode) 341 | { 342 | switch (slashMode) 343 | { 344 | case SlashMode.Backward: 345 | return '\\'; 346 | case SlashMode.Forward: 347 | return '/'; 348 | default: 349 | return Path.DirectorySeparatorChar; 350 | } 351 | } 352 | 353 | public override bool Equals(Object obj) 354 | { 355 | if (obj == null) 356 | return false; 357 | 358 | // If parameter cannot be cast to Point return false. 359 | var p = obj as NPath; 360 | if ((Object)p == null) 361 | return false; 362 | 363 | return Equals(p); 364 | } 365 | 366 | public bool Equals(NPath p) 367 | { 368 | if (p._isRelative != _isRelative) 369 | return false; 370 | 371 | if (!string.Equals(p._driveLetter, _driveLetter, PathStringComparison)) 372 | return false; 373 | 374 | if (p._elements.Length != _elements.Length) 375 | return false; 376 | 377 | for (var i = 0; i != _elements.Length; i++) 378 | if (!string.Equals(p._elements[i], _elements[i], PathStringComparison)) 379 | return false; 380 | 381 | return true; 382 | } 383 | 384 | public static bool operator==(NPath a, NPath b) 385 | { 386 | // If both are null, or both are same instance, return true. 387 | if (ReferenceEquals(a, b)) 388 | return true; 389 | 390 | // If one is null, but not both, return false. 391 | if (((object)a == null) || ((object)b == null)) 392 | return false; 393 | 394 | // Return true if the fields match: 395 | return a.Equals(b); 396 | } 397 | 398 | public override int GetHashCode() 399 | { 400 | unchecked 401 | { 402 | int hash = 17; 403 | // Suitable nullity checks etc, of course :) 404 | hash = hash * 23 + _isRelative.GetHashCode(); 405 | foreach (var element in _elements) 406 | hash = hash * 23 + element.GetHashCode(); 407 | if (_driveLetter != null) 408 | hash = hash * 23 + _driveLetter.GetHashCode(); 409 | return hash; 410 | } 411 | } 412 | 413 | public int CompareTo(object obj) 414 | { 415 | if (obj == null) 416 | return -1; 417 | 418 | return this.ToString().CompareTo(((NPath)obj).ToString()); 419 | } 420 | 421 | public static bool operator!=(NPath a, NPath b) 422 | { 423 | return !(a == b); 424 | } 425 | 426 | public bool HasExtension(params string[] extensions) 427 | { 428 | var extensionWithDotLower = ExtensionWithDot.ToLower(); 429 | return extensions.Any(e => WithDot(e).ToLower() == extensionWithDotLower); 430 | } 431 | 432 | private static string WithDot(string extension) 433 | { 434 | return extension.StartsWith(".") ? extension : "." + extension; 435 | } 436 | 437 | private bool IsEmpty() 438 | { 439 | return _elements.Length == 0; 440 | } 441 | 442 | public bool IsRoot 443 | { 444 | get { return _elements.Length == 0 && !_isRelative; } 445 | } 446 | 447 | #endregion inspection 448 | 449 | #region directory enumeration 450 | 451 | public IEnumerable Files(string filter, bool recurse = false) 452 | { 453 | return Directory.GetFiles(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); 454 | } 455 | 456 | public IEnumerable Files(bool recurse = false) 457 | { 458 | return Files("*", recurse); 459 | } 460 | 461 | public IEnumerable Contents(string filter, bool recurse = false) 462 | { 463 | return Files(filter, recurse).Concat(Directories(filter, recurse)); 464 | } 465 | 466 | public IEnumerable Contents(bool recurse = false) 467 | { 468 | return Contents("*", recurse); 469 | } 470 | 471 | public IEnumerable Directories(string filter, bool recurse = false) 472 | { 473 | return Directory.GetDirectories(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); 474 | } 475 | 476 | public IEnumerable Directories(bool recurse = false) 477 | { 478 | return Directories("*", recurse); 479 | } 480 | 481 | #endregion 482 | 483 | #region filesystem writing operations 484 | public NPath CreateFile() 485 | { 486 | ThrowIfRelative(); 487 | ThrowIfRoot(); 488 | EnsureParentDirectoryExists(); 489 | File.WriteAllBytes(ToString(), new byte[0]); 490 | return this; 491 | } 492 | 493 | public NPath CreateFile(string file) 494 | { 495 | return CreateFile(new NPath(file)); 496 | } 497 | 498 | public NPath CreateFile(NPath file) 499 | { 500 | if (!file.IsRelative) 501 | throw new ArgumentException("You cannot call CreateFile() on an existing path with a non relative argument"); 502 | return Combine(file).CreateFile(); 503 | } 504 | 505 | public NPath CreateDirectory() 506 | { 507 | ThrowIfRelative(); 508 | 509 | if (IsRoot) 510 | throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString()); 511 | 512 | Directory.CreateDirectory(ToString()); 513 | return this; 514 | } 515 | 516 | public NPath CreateDirectory(string directory) 517 | { 518 | return CreateDirectory(new NPath(directory)); 519 | } 520 | 521 | public NPath CreateDirectory(NPath directory) 522 | { 523 | if (!directory.IsRelative) 524 | throw new ArgumentException("Cannot call CreateDirectory with an absolute argument"); 525 | 526 | return Combine(directory).CreateDirectory(); 527 | } 528 | 529 | public NPath Copy(string dest) 530 | { 531 | return Copy(new NPath(dest)); 532 | } 533 | 534 | public NPath Copy(string dest, Func fileFilter) 535 | { 536 | return Copy(new NPath(dest), fileFilter); 537 | } 538 | 539 | public NPath Copy(NPath dest) 540 | { 541 | return Copy(dest, p => true); 542 | } 543 | 544 | public NPath Copy(NPath dest, Func fileFilter) 545 | { 546 | ThrowIfRelative(); 547 | if (dest.IsRelative) 548 | dest = Parent.Combine(dest); 549 | 550 | if (dest.DirectoryExists()) 551 | return CopyWithDeterminedDestination(dest.Combine(FileName), fileFilter); 552 | 553 | return CopyWithDeterminedDestination(dest, fileFilter); 554 | } 555 | 556 | public NPath MakeAbsolute() 557 | { 558 | if (!IsRelative) 559 | return this; 560 | 561 | return NPath.CurrentDirectory.Combine(this); 562 | } 563 | 564 | NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func fileFilter) 565 | { 566 | if (absoluteDestination.IsRelative) 567 | throw new ArgumentException("absoluteDestination must be absolute"); 568 | 569 | if (FileExists()) 570 | { 571 | if (!fileFilter(absoluteDestination)) 572 | return null; 573 | 574 | absoluteDestination.EnsureParentDirectoryExists(); 575 | 576 | File.Copy(ToString(), absoluteDestination.ToString(), true); 577 | return absoluteDestination; 578 | } 579 | 580 | if (DirectoryExists()) 581 | { 582 | absoluteDestination.EnsureDirectoryExists(); 583 | foreach (var thing in Contents()) 584 | thing.CopyWithDeterminedDestination(absoluteDestination.Combine(thing.RelativeTo(this)), fileFilter); 585 | return absoluteDestination; 586 | } 587 | 588 | throw new ArgumentException("Copy() called on path that doesnt exist: " + ToString()); 589 | } 590 | 591 | public void Delete(DeleteMode deleteMode = DeleteMode.Normal) 592 | { 593 | ThrowIfRelative(); 594 | 595 | if (IsRoot) 596 | throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString()); 597 | 598 | if (FileExists()) 599 | File.Delete(ToString()); 600 | else if (DirectoryExists()) 601 | try 602 | { 603 | Directory.Delete(ToString(), true); 604 | } 605 | catch (IOException) 606 | { 607 | if (deleteMode == DeleteMode.Normal) 608 | throw; 609 | } 610 | else 611 | throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString()); 612 | } 613 | 614 | public void DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal) 615 | { 616 | ThrowIfRelative(); 617 | 618 | if (FileExists() || DirectoryExists()) 619 | Delete(deleteMode); 620 | } 621 | 622 | public NPath DeleteContents() 623 | { 624 | ThrowIfRelative(); 625 | 626 | if (IsRoot) 627 | throw new NotSupportedException("DeleteContents is not supported on a root level directory because it would be dangerous:" + ToString()); 628 | 629 | if (FileExists()) 630 | throw new InvalidOperationException("It is not valid to perform this operation on a file"); 631 | 632 | if (DirectoryExists()) 633 | { 634 | try 635 | { 636 | Files().Delete(); 637 | Directories().Delete(); 638 | } 639 | catch (IOException) 640 | { 641 | if (Files(true).Any()) 642 | throw; 643 | } 644 | 645 | return this; 646 | } 647 | 648 | return EnsureDirectoryExists(); 649 | } 650 | 651 | public static NPath CreateTempDirectory(string myprefix) 652 | { 653 | var random = new Random(); 654 | while (true) 655 | { 656 | var candidate = new NPath(Path.GetTempPath() + "/" + myprefix + "_" + random.Next()); 657 | if (!candidate.Exists()) 658 | return candidate.CreateDirectory(); 659 | } 660 | } 661 | 662 | public NPath Move(string dest) 663 | { 664 | return Move(new NPath(dest)); 665 | } 666 | 667 | public NPath Move(NPath dest) 668 | { 669 | ThrowIfRelative(); 670 | 671 | if (IsRoot) 672 | throw new NotSupportedException("Move is not supported on a root level directory because it would be dangerous:" + ToString()); 673 | 674 | if (dest.IsRelative) 675 | return Move(Parent.Combine(dest)); 676 | 677 | if (dest.DirectoryExists()) 678 | return Move(dest.Combine(FileName)); 679 | 680 | if (FileExists()) 681 | { 682 | dest.EnsureParentDirectoryExists(); 683 | File.Move(ToString(), dest.ToString()); 684 | return dest; 685 | } 686 | 687 | if (DirectoryExists()) 688 | { 689 | Directory.Move(ToString(), dest.ToString()); 690 | return dest; 691 | } 692 | 693 | throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString()); 694 | } 695 | 696 | #endregion 697 | 698 | #region special paths 699 | 700 | public static NPath CurrentDirectory 701 | { 702 | get 703 | { 704 | return new NPath(Directory.GetCurrentDirectory()); 705 | } 706 | } 707 | 708 | public static NPath HomeDirectory 709 | { 710 | get 711 | { 712 | if (Path.DirectorySeparatorChar == '\\') 713 | return new NPath(Environment.GetEnvironmentVariable("USERPROFILE")); 714 | return new NPath(Environment.GetEnvironmentVariable("HOME")); 715 | } 716 | } 717 | 718 | public static NPath SystemTemp 719 | { 720 | get 721 | { 722 | return new NPath(Path.GetTempPath()); 723 | } 724 | } 725 | 726 | #endregion 727 | 728 | public void ThrowIfRelative() 729 | { 730 | if (_isRelative) 731 | throw new ArgumentException("You are attempting an operation on a Path that requires an absolute path, but the path is relative"); 732 | } 733 | 734 | public void ThrowIfRoot() 735 | { 736 | if (IsRoot) 737 | throw new ArgumentException("You are attempting an operation that is not valid on a root level directory"); 738 | } 739 | 740 | public NPath EnsureDirectoryExists(string append = "") 741 | { 742 | return EnsureDirectoryExists(new NPath(append)); 743 | } 744 | 745 | public NPath EnsureDirectoryExists(NPath append) 746 | { 747 | var combined = Combine(append); 748 | if (combined.DirectoryExists()) 749 | return combined; 750 | combined.EnsureParentDirectoryExists(); 751 | combined.CreateDirectory(); 752 | return combined; 753 | } 754 | 755 | public NPath EnsureParentDirectoryExists() 756 | { 757 | var parent = Parent; 758 | parent.EnsureDirectoryExists(); 759 | return parent; 760 | } 761 | 762 | public NPath FileMustExist() 763 | { 764 | if (!FileExists()) 765 | throw new FileNotFoundException("File was expected to exist : " + ToString()); 766 | 767 | return this; 768 | } 769 | 770 | public NPath DirectoryMustExist() 771 | { 772 | if (!DirectoryExists()) 773 | throw new DirectoryNotFoundException("Expected directory to exist : " + ToString()); 774 | 775 | return this; 776 | } 777 | 778 | public bool IsChildOf(string potentialBasePath) 779 | { 780 | return IsChildOf(new NPath(potentialBasePath)); 781 | } 782 | 783 | public bool IsChildOf(NPath potentialBasePath) 784 | { 785 | if ((IsRelative && !potentialBasePath.IsRelative) || !IsRelative && potentialBasePath.IsRelative) 786 | throw new ArgumentException("You can only call IsChildOf with two relative paths, or with two absolute paths"); 787 | 788 | // If the other path is the root directory, then anything is a child of it as long as it's not a Windows path 789 | if (potentialBasePath.IsRoot) 790 | { 791 | if (_driveLetter != potentialBasePath._driveLetter) 792 | return false; 793 | return true; 794 | } 795 | 796 | if (IsEmpty()) 797 | return false; 798 | 799 | if (Equals(potentialBasePath)) 800 | return true; 801 | 802 | return Parent.IsChildOf(potentialBasePath); 803 | } 804 | 805 | public IEnumerable RecursiveParents 806 | { 807 | get 808 | { 809 | var candidate = this; 810 | while (true) 811 | { 812 | if (candidate.IsEmpty()) 813 | yield break; 814 | 815 | candidate = candidate.Parent; 816 | yield return candidate; 817 | } 818 | } 819 | } 820 | 821 | public NPath ParentContaining(string needle, bool returnAppended = false) 822 | { 823 | return ParentContaining(new NPath(needle), returnAppended); 824 | } 825 | 826 | public NPath ParentContaining(NPath needle, bool returnAppended = false) 827 | { 828 | ThrowIfRelative(); 829 | 830 | var found = RecursiveParents.FirstOrDefault(p => p.Exists(needle)); 831 | if (returnAppended && found != null) 832 | found = found.Combine(needle); 833 | 834 | return found; 835 | } 836 | 837 | public NPath WriteAllText(string contents) 838 | { 839 | ThrowIfRelative(); 840 | EnsureParentDirectoryExists(); 841 | File.WriteAllText(ToString(), contents); 842 | return this; 843 | } 844 | 845 | public string ReadAllText() 846 | { 847 | ThrowIfRelative(); 848 | return File.ReadAllText(ToString()); 849 | } 850 | 851 | public NPath WriteAllLines(params string[] contents) 852 | { 853 | ThrowIfRelative(); 854 | EnsureParentDirectoryExists(); 855 | File.WriteAllLines(ToString(), contents); 856 | return this; 857 | } 858 | 859 | public string[] ReadAllLines() 860 | { 861 | ThrowIfRelative(); 862 | return File.ReadAllLines(ToString()); 863 | } 864 | 865 | public IEnumerable CopyFiles(NPath destination, bool recurse, Func fileFilter = null) 866 | { 867 | destination.EnsureDirectoryExists(); 868 | return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Copy(destination.Combine(file.RelativeTo(this)))).ToArray(); 869 | } 870 | 871 | public IEnumerable MoveFiles(NPath destination, bool recurse, Func fileFilter = null) 872 | { 873 | if (IsRoot) 874 | throw new NotSupportedException("MoveFiles is not supported on this directory because it would be dangerous:" + ToString()); 875 | 876 | destination.EnsureDirectoryExists(); 877 | return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Move(destination.Combine(file.RelativeTo(this)))).ToArray(); 878 | } 879 | 880 | static bool AlwaysTrue(NPath p) 881 | { 882 | return true; 883 | } 884 | 885 | private static bool IsLinux() 886 | { 887 | return Directory.Exists("/proc"); 888 | } 889 | } 890 | 891 | public static class Extensions 892 | { 893 | public static IEnumerable Copy(this IEnumerable self, string dest) 894 | { 895 | return Copy(self, new NPath(dest)); 896 | } 897 | 898 | public static IEnumerable Copy(this IEnumerable self, NPath dest) 899 | { 900 | if (dest.IsRelative) 901 | throw new ArgumentException("When copying multiple files, the destination cannot be a relative path"); 902 | dest.EnsureDirectoryExists(); 903 | return self.Select(p => p.Copy(dest.Combine(p.FileName))).ToArray(); 904 | } 905 | 906 | public static IEnumerable Move(this IEnumerable self, string dest) 907 | { 908 | return Move(self, new NPath(dest)); 909 | } 910 | 911 | public static IEnumerable Move(this IEnumerable self, NPath dest) 912 | { 913 | if (dest.IsRelative) 914 | throw new ArgumentException("When moving multiple files, the destination cannot be a relative path"); 915 | dest.EnsureDirectoryExists(); 916 | return self.Select(p => p.Move(dest.Combine(p.FileName))).ToArray(); 917 | } 918 | 919 | public static IEnumerable Delete(this IEnumerable self) 920 | { 921 | foreach (var p in self) 922 | p.Delete(); 923 | return self; 924 | } 925 | 926 | public static IEnumerable InQuotes(this IEnumerable self, SlashMode forward = SlashMode.Native) 927 | { 928 | return self.Select(p => p.InQuotes(forward)); 929 | } 930 | 931 | public static NPath ToNPath(this string path) 932 | { 933 | return new NPath(path); 934 | } 935 | } 936 | 937 | public enum SlashMode 938 | { 939 | Native, 940 | Forward, 941 | Backward 942 | } 943 | 944 | public enum DeleteMode 945 | { 946 | Normal, 947 | Soft 948 | } 949 | } 950 | -------------------------------------------------------------------------------- /source/Unity.Core/ProcessUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading; 6 | 7 | namespace Unity.Core 8 | { 9 | public class ProcessUtility 10 | { 11 | public enum StdStream 12 | { 13 | Stdout, 14 | Stderr 15 | } 16 | 17 | public static int ExecuteCommandLine( 18 | string exePath, IEnumerable processArgs, string workingDirectory, 19 | Action onLine, IEnumerable stdinLines = null) 20 | { 21 | if (processArgs == null) 22 | processArgs = Enumerable.Empty(); 23 | 24 | var processArgsText = processArgs 25 | .Select(obj => 26 | { 27 | var str = obj.ToString(); 28 | if (str.IndexOf(' ') >= 0) 29 | str = '"' + str + '"'; 30 | return str; 31 | }) 32 | .StringJoin(" "); 33 | 34 | using (var stdoutCompleted = new ManualResetEvent(false)) 35 | using (var stderrCompleted = new ManualResetEvent(false)) 36 | using (var process = new Process 37 | { 38 | StartInfo = new ProcessStartInfo 39 | { 40 | // keep new process completely out of user view 41 | UseShellExecute = false, 42 | CreateNoWindow = true, 43 | WindowStyle = ProcessWindowStyle.Hidden, 44 | ErrorDialog = false, 45 | 46 | WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory, 47 | FileName = exePath, 48 | Arguments = processArgsText, 49 | 50 | RedirectStandardInput = stdinLines != null, 51 | RedirectStandardOutput = true, 52 | RedirectStandardError = true, 53 | } 54 | }) 55 | { 56 | // avoid caller needing to do this (and pretty much everybody will want it) 57 | var serializer = new object(); 58 | 59 | // ReSharper disable AccessToDisposedClosure 60 | // ^ this is ok because we either kill or wait for process to stop before `using` will dispose the events 61 | process.OutputDataReceived += (_, line) => 62 | { 63 | if (line.Data == null) 64 | stdoutCompleted.Set(); 65 | else 66 | { 67 | lock (serializer) 68 | onLine(line.Data, StdStream.Stdout); 69 | } 70 | }; 71 | process.ErrorDataReceived += (_, line) => 72 | { 73 | if (line.Data == null) 74 | stderrCompleted.Set(); 75 | else 76 | { 77 | lock (serializer) 78 | onLine(line.Data, StdStream.Stderr); 79 | } 80 | }; 81 | 82 | // ReSharper restore AccessToDisposedClosure 83 | 84 | // start everything 85 | process.Start(); 86 | process.BeginOutputReadLine(); 87 | process.BeginErrorReadLine(); 88 | 89 | // write if needed 90 | if (stdinLines != null) 91 | { 92 | foreach (var line in stdinLines) 93 | process.StandardInput.WriteLine(line); 94 | 95 | process.StandardInput.Close(); 96 | } 97 | 98 | // wait for proc and all reads to finish 99 | process.WaitForExit(); 100 | stdoutCompleted.WaitOne(); 101 | stderrCompleted.WaitOne(); 102 | 103 | return process.ExitCode; 104 | } 105 | } 106 | 107 | public static int ExecuteCommandLine( 108 | string exePath, IEnumerable processArgs, string workingDirectory, 109 | ICollection stdout, ICollection stderr, IEnumerable stdin = null) 110 | { 111 | return ExecuteCommandLine( 112 | exePath, processArgs, workingDirectory, 113 | (line, stream) => (stream == StdStream.Stdout ? stdout : stderr)?.Add(line), 114 | stdin); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /source/Unity.Core/SafeFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Unity.Core 5 | { 6 | public static class SafeFile 7 | { 8 | // TODO: add tests (see https://stackoverflow.com/a/1528151/14582) 9 | public static void AtomicWrite(string path, Action write) 10 | { 11 | // note that File.Delete doesn't throw if file doesn't exist 12 | 13 | // dotnet doesn't have an atomic move operation (have to pinvoke to something in the OS to get that, 14 | // and even then on windows it's not guaranteed). so the "atomic" part of this name is just to ensure 15 | // that partially written file never happens. 16 | 17 | var tmpPath = path + ".tmp"; 18 | 19 | try 20 | { 21 | File.Delete(tmpPath); 22 | write(tmpPath); 23 | 24 | // temporarily keep the old file, until we're sure the new file is moved 25 | var bakPath = path + ".bak"; 26 | File.Delete(bakPath); 27 | File.Move(path, bakPath); 28 | 29 | File.Move(tmpPath, path); 30 | 31 | // now the old one can go away 32 | // FUTURE: based on option to func, keep bak file 33 | File.Delete(bakPath); 34 | } 35 | finally 36 | { 37 | try 38 | { 39 | File.Delete(tmpPath); 40 | } 41 | catch 42 | { 43 | // failure to cleanup a tmp file isn't critical 44 | } 45 | } 46 | 47 | // FUTURE: options to throw on existing/auto-overwrite 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/Unity.Core/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using JetBrains.Annotations; 7 | 8 | namespace Unity.Core 9 | { 10 | public static class StringExtensions 11 | { 12 | [ContractAnnotation("null=>true", true), Pure] 13 | public static bool IsNullOrEmpty([CanBeNull] this string @this) => string.IsNullOrEmpty(@this); 14 | [ContractAnnotation("null=>true", true), Pure] 15 | public static bool IsNullOrWhiteSpace([CanBeNull] this string @this) => string.IsNullOrWhiteSpace(@this); 16 | 17 | public static bool IsEmpty([NotNull] this string @this) => @this.Length == 0; 18 | public static bool Any([NotNull] this string @this) => @this.Length != 0; 19 | 20 | // left/mid/right are 'basic' inspired names, and never throw 21 | 22 | [NotNull] 23 | public static string Left([NotNull] this string @this, int maxChars) 24 | => @this.Substring(0, Math.Min(maxChars, @this.Length)); 25 | 26 | [NotNull] 27 | public static string Mid([NotNull] this string @this, int offset, int maxChars = -1) 28 | { 29 | if (offset < 0) 30 | throw new ArgumentException("offset must be >= 0", nameof(offset)); 31 | 32 | var safeOffset = offset.Clamp(0, @this.Length); 33 | var actualMaxChars = @this.Length - safeOffset; 34 | 35 | var safeMaxChars = maxChars < 0 ? actualMaxChars : Math.Min(maxChars, actualMaxChars); 36 | 37 | return @this.Substring(safeOffset, safeMaxChars); 38 | } 39 | 40 | [NotNull] 41 | public static string Right([NotNull] this string @this, int maxChars) 42 | { 43 | var safeMaxChars = Math.Min(maxChars, @this.Length); 44 | return @this.Substring(@this.Length - safeMaxChars, safeMaxChars); 45 | } 46 | 47 | [NotNull] 48 | public static string Truncate([NotNull] this string @this, int maxChars, string trailer = "...") 49 | { 50 | if (@this.Length <= maxChars) 51 | return @this; 52 | 53 | return @this.Left(maxChars - trailer.Length) + trailer; 54 | } 55 | 56 | [NotNull] 57 | public static string StringJoin([NotNull] this IEnumerable @this, [NotNull] string separator) 58 | => string.Join(separator, @this.Cast()); 59 | 60 | [NotNull] 61 | public static string StringJoin([NotNull] this IEnumerable @this, char separator) 62 | => string.Join(new string(separator, 1), @this.Cast()); 63 | 64 | [NotNull] 65 | public static string StringJoin([NotNull] this IEnumerable @this, [NotNull] Func selector, [NotNull] string separator) 66 | => string.Join(separator, @this.Select(selector)); 67 | 68 | [NotNull] 69 | public static string StringJoin([NotNull] this IEnumerable @this, [NotNull] Func selector, char separator) 70 | => string.Join(new string(separator, 1), @this.Select(selector)); 71 | 72 | [NotNull] 73 | public static IEnumerable ToLower([NotNull] this IEnumerable @this) 74 | => @this.Select(s => s.ToLower()); 75 | 76 | [NotNull] 77 | public static IEnumerable ToUpper([NotNull] this IEnumerable @this) 78 | => @this.Select(s => s.ToUpper()); 79 | 80 | // the buffer is for avoiding the builder alloc each time. useful when processing multiple lines, and can cut allocs by half. 81 | [NotNull] 82 | public static string ExpandTabs([NotNull] this string @this, int tabWidth, StringBuilder buffer = null) 83 | { 84 | if (tabWidth < 0) 85 | throw new ArgumentException("tabWidth must be >= 0", nameof(tabWidth)); 86 | 87 | var tabCount = @this.Count(c => c == '\t'); 88 | 89 | // early out if nothing to do 90 | if (tabCount == 0) 91 | return @this; 92 | 93 | // more early-out and a bit silly scenarios, but why not.. 94 | if (tabWidth == 0) 95 | return @this.Replace("\t", ""); 96 | if (tabWidth == 1) 97 | return @this.Replace('\t', ' '); 98 | 99 | var capacity = @this.Length + tabCount * (tabWidth - 1); 100 | if (buffer != null) 101 | buffer.EnsureCapacity(capacity); 102 | else 103 | buffer = new StringBuilder(capacity); 104 | 105 | foreach (var c in @this) 106 | { 107 | if (c != '\t') 108 | buffer.Append(c); 109 | else 110 | buffer.Append(' ', tabWidth - buffer.Length % tabWidth); 111 | } 112 | 113 | var expanded = buffer.ToString(); 114 | buffer.Clear(); 115 | return expanded; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /source/Unity.Core/Unity.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(Unity_DotNetTargetFrameworks) 5 | 6 | 7 | 8 | 2018.2.1 9 | 10 | 11 | 4.5.0 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /source/common.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(DefaultItemExcludes);**\*.bak;**\*.orig 4 | net461 5 | netstandard2.0 6 | 7 | 8 | DEBUG;TRACE 9 | Full 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/NSubstitute.Elevated.Tests/BasicTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security; 3 | using SystemUnderTest; 4 | using NiceIO; 5 | using NSubstitute.Exceptions; 6 | using NUnit.Framework; 7 | using Shouldly; 8 | 9 | namespace NSubstitute.Elevated.Tests 10 | { 11 | class BasicTests 12 | { 13 | IDisposable m_Dispose; 14 | 15 | [OneTimeSetUp] 16 | public void Setup() 17 | { 18 | var buildFolder = new NPath(GetType().Assembly.Location).Parent; 19 | var systemUnderTest = buildFolder.Combine("SystemUnderTest.dll"); // do not access type directly, want to avoid loading the assembly until it's patched 20 | 21 | m_Dispose = ElevatedSubstitutionContext.AutoHook(systemUnderTest); 22 | } 23 | 24 | [OneTimeTearDown] 25 | public void TearDown() 26 | { 27 | m_Dispose.Dispose(); 28 | } 29 | 30 | [Test] 31 | public void MockByInterface_ShouldUseNSubDefaultBehavior() 32 | { 33 | var sub = Substitute.For(); 34 | 35 | sub.GetType().FullName.ShouldBe("Castle.Proxies.IBasicInterfaceProxy"); 36 | sub.GetValue().ShouldBe(0); 37 | 38 | sub.GetValue().Returns(5); 39 | 40 | sub.GetValue().ShouldBe(5); 41 | } 42 | 43 | [Test] 44 | public void MockByVirtualClass_ShouldUseNSubDefaultBehavior() 45 | { 46 | var sub = Substitute.ForPartsOf(); 47 | 48 | sub.GetType().FullName.ShouldBe("Castle.Proxies.ClassWithVirtualsProxy"); 49 | sub.GetValue().ShouldBe(4); 50 | 51 | sub.GetValue().Returns(6); 52 | 53 | sub.GetValue().ShouldBe(6); 54 | } 55 | 56 | [Test] 57 | public void ClassWithDefaultCtor_MockedMethod_ShouldNotRun() 58 | { 59 | var sub = Substitute.For(); 60 | 61 | sub.ShouldBeOfType(); 62 | sub.Value.ShouldBe(0); 63 | } 64 | 65 | [Test] 66 | public void ClassWithDefaultCtor_MockedMethod_ReturnsOverriddenValue() 67 | { 68 | var sub = Substitute.For(); 69 | 70 | sub.Value.Returns(24); 71 | 72 | sub.Value.ShouldBe(24); 73 | } 74 | 75 | [Test] 76 | public void ClassWithNoDefaultCtor_TypeDoesNotChange() 77 | { 78 | var sub = Substitute.For(); 79 | 80 | sub.ShouldBeOfType(); 81 | } 82 | 83 | [Test] 84 | public void ClassWithICallInCtor_ThrowsOnInstantiation() 85 | { 86 | // this just verifies that we actually have an icall compiled in 87 | 88 | // ReSharper disable once ObjectCreationAsStatement 89 | Should.Throw(() => new ClassWithCtorICall()); 90 | } 91 | 92 | [Test] 93 | public void ClassWithICallInCtor_TypeDoesNotChange() 94 | { 95 | // $ TODO: make this into an actual test of the icall thing. currently just checks that doesn't throw..not that interesting 96 | 97 | var sub = Substitute.For(); 98 | 99 | sub.ShouldBeOfType(); 100 | } 101 | 102 | [Test] 103 | public void ClassWithThrowInCtor_TypeDoesNotChange() 104 | { 105 | var sub = Substitute.For(); 106 | 107 | sub.ShouldBeOfType(); 108 | } 109 | 110 | [Test] 111 | public void ClassWithCtorParams_WhenMocked_ShouldThrow() 112 | { 113 | // TODO: why commented out? 114 | // Should.Throw(() => Substitute.For()); 115 | Should.Throw(() => Substitute.For("test")); 116 | Should.Throw(() => Substitute.For(null, null)); 117 | } 118 | 119 | [Test] 120 | public void ClassWithNoMethods_ShouldBeIgnoredByWeaver() 121 | { 122 | // if it's a patched type, mocking will produce identical type (i.e. proxying installed directly in type). 123 | // if unpatched, then mocking will run standard nsubstitute behavior (i.e. proxying done via dynamicproxy generator, which inherits proxy type from the real type). 124 | 125 | var subEmpty = Substitute.For(); 126 | subEmpty.GetType().ShouldBe(typeof(EmptyClass)); 127 | 128 | var subNoCtor1 = Substitute.For(null); 129 | subNoCtor1.GetType().ShouldBe(typeof(ClassWithNoDefaultCtorNoMethods)); 130 | 131 | // TODO: why commented out? 132 | // var subNoCtor2 = Substitute.For("test"); TODO: This will cause an exception as ForPartsOf should be used. Maybe do that here instead? 133 | // subNoCtor2.GetType().ShouldBe(typeof(ClassWithNoDefaultCtorNoMethods)); 134 | 135 | // var subNoCtor3 = Substitute.For(null, null); 136 | // subNoCtor3.GetType().ShouldBe(typeof(ClassWithNoDefaultCtorNoMethods)); 137 | } 138 | 139 | [Test] 140 | public void NonMockedClassWithDependentTypes_Loads() 141 | { 142 | // ReSharper disable once PossibleNullReferenceException 143 | typeof(ClassWithDependency).GetMethod("Dummy").ReturnType.FullName.ShouldBe("DependentAssembly.DependentType"); 144 | } 145 | 146 | [Test] 147 | public void ClassWithDependentTypes_CanUseDependentAssemblies() 148 | { 149 | // simple test to ensure that we can patch methods that use types from foreign assemblies 150 | 151 | var sub = Substitute.For(); 152 | 153 | // ReSharper disable once PossibleNullReferenceException 154 | sub.GetType().GetMethod("Dummy").ReturnType.FullName.ShouldBe("DependentAssembly.DependentType"); 155 | 156 | // $$$ TODO: test that the type is itself patched (look for __mockthingy) 157 | } 158 | 159 | [Test] 160 | public void SimpleClass_FullMock_DoesNotCallDefaultImpls() 161 | { 162 | var sub = Substitute.For(); 163 | 164 | sub.VoidMethodWithParam(5); 165 | sub.Modified.ShouldBe(0); 166 | 167 | sub.ReturnMethodWithParam(5).ShouldBe(0); 168 | sub.Modified.ShouldBe(0); 169 | 170 | sub.ReturnMethodWithParam(5).Returns(10); 171 | sub.ReturnMethodWithParam(5).ShouldBe(10); 172 | sub.Modified.ShouldBe(0); 173 | } 174 | 175 | [Test] 176 | public void SimpleClass_PartialMock_CallsDefaultImpls() 177 | { 178 | var sub = Substitute.ForPartsOf(); 179 | 180 | sub.VoidMethodWithParam(5); 181 | sub.Modified.ShouldBe(5); 182 | 183 | sub.ReturnMethodWithParam(3).ShouldBe(8); 184 | sub.Modified.ShouldBe(8); 185 | 186 | sub.ReturnMethodWithParam(4).Returns(10); 187 | sub.ReturnMethodWithParam(4).ShouldBe(10); 188 | sub.Modified.ShouldBe(8); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/NSubstitute.Elevated.Tests/ElevatedWeaverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Mono.Cecil; 4 | using Mono.Cecil.Rocks; 5 | using NSubstitute.Elevated.Weaver; 6 | using NSubstitute.Elevated.WeaverInternals; 7 | using NUnit.Framework; 8 | using Shouldly; 9 | 10 | namespace NSubstitute.Elevated.Tests.Utilities 11 | { 12 | public class DependentAssemblyTests : PatchingFixture 13 | { 14 | [Test] 15 | public void PatchingDependentAssembly_WhenAlreadyLoaded_ShouldThrow() 16 | {/* 17 | var dependentDllName = GetType().Name + "_dependent"; 18 | var dependentAssemblyPath = Compile(BaseDir, dependentDllName, "public class ReferencedType { }" ); 19 | var usingAssemblyPath = Compile(BaseDir, GetType().Name + "_using", "public class UsingType : ReferencedType { }", dependentDllName); 20 | 21 | var dependentAssembly = PatchAndValidateAllDependentAssemblies(dependentAssemblyPath); 22 | var type = GetType(dependentAssembly, "ReferencedType"); 23 | MockInjector.IsPatched(type).ShouldBeFalse();*/ 24 | } 25 | } 26 | 27 | // TODO: tests to add 28 | // 29 | // sample: CombinatorialAttribute : CombiningStrategyAttribute << this crashes 30 | // class Base { Base(int) { } } 31 | // class Derived : Base { Derived() : base(1) { } } 32 | // 33 | // class with `public delegate void Blah();` 34 | // 35 | // patching a system (signed) assembly 36 | 37 | public class ElevatedWeaverTests : PatchingFixture 38 | { 39 | AssemblyDefinition m_TestAssembly; 40 | 41 | const string k_FixtureTestCode = @" 42 | 43 | using System; 44 | using System.Collections.Generic; 45 | using System.Runtime.InteropServices; 46 | 47 | namespace ShouldNotPatch 48 | { 49 | interface Interface { void Foo(); } // ordinary proxying works 50 | 51 | [StructLayout(LayoutKind.Explicit, Size=4)] // size is necessary to make peverify happy when using StructLayout 52 | struct StructWithLayoutAttr { } // don't patch these because adding a field may ruin serialization or blitting or something 53 | 54 | class ClassWithPrivateNestedType 55 | { 56 | class PrivateNested { } // unavailable externally, no point 57 | } 58 | 59 | class ClassWithGeneratedNestedType 60 | { 61 | public IEnumerable Foo() // this causes a state machine type to be generated which shouldn't be patched 62 | { yield return 1; } 63 | } 64 | } 65 | 66 | namespace ShouldPatch 67 | { 68 | class ClassWithNestedTypes 69 | { 70 | public class PublicNested { } 71 | internal class InternalNested { } 72 | } 73 | } 74 | "; 75 | 76 | [OneTimeSetUp] 77 | public void OneTimeSetUp() 78 | { 79 | var testAssemblyPath = Compile(GetType().Name, k_FixtureTestCode); 80 | 81 | var results = ElevatedWeaver.PatchAllDependentAssemblies(testAssemblyPath, PatchOptions.PatchTestAssembly); 82 | results.Count.ShouldBe(2); 83 | results.ShouldContain(new PatchResult("mscorlib", null, PatchState.IgnoredForeignAssembly)); 84 | results.ShouldContain(new PatchResult(testAssemblyPath, ElevatedWeaver.GetPatchBackupPathFor(testAssemblyPath), PatchState.Patched)); 85 | 86 | m_TestAssembly = AssemblyDefinition.ReadAssembly(testAssemblyPath); 87 | MockInjector.IsPatched(m_TestAssembly).ShouldBeTrue(); 88 | } 89 | 90 | [OneTimeTearDown] 91 | public void OneTimeTearDown() 92 | { 93 | m_TestAssembly?.Dispose(); 94 | } 95 | 96 | [Test] 97 | public void Interfaces_ShouldNotPatch() 98 | { 99 | var type = GetType(m_TestAssembly, "ShouldNotPatch.Interface"); 100 | MockInjector.IsPatched(type).ShouldBeFalse(); 101 | } 102 | 103 | [Test] 104 | public void PotentiallyBlittableStructs_ShouldNotPatch() 105 | { 106 | var type = GetType(m_TestAssembly, "ShouldNotPatch.StructWithLayoutAttr"); 107 | MockInjector.IsPatched(type).ShouldBeFalse(); 108 | } 109 | 110 | [Test] 111 | public void PrivateNestedTypes_ShouldNotPatch() 112 | { 113 | var type = GetType(m_TestAssembly, "ShouldNotPatch.ClassWithPrivateNestedType/PrivateNested"); 114 | MockInjector.IsPatched(type).ShouldBeFalse(); 115 | } 116 | 117 | [Test] 118 | public void GeneratedTypes_ShouldNotPatch() 119 | { 120 | var type = GetType(m_TestAssembly, "ShouldNotPatch.ClassWithGeneratedNestedType"); 121 | type.NestedTypes.Count.ShouldBe(1); // this is the yield state machine, will be mangled name 122 | MockInjector.IsPatched(type.NestedTypes[0]).ShouldBeFalse(); 123 | } 124 | 125 | [Test] 126 | public void TopLevelClass_ShouldPatch() 127 | { 128 | var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes"); 129 | MockInjector.IsPatched(type).ShouldBeTrue(); 130 | } 131 | 132 | [Test] 133 | public void PublicNestedClasses_ShouldPatch() 134 | { 135 | var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes/PublicNested"); 136 | MockInjector.IsPatched(type).ShouldBeTrue(); 137 | } 138 | 139 | [Test] 140 | public void InternalNestedClasses_ShouldPatch() 141 | { 142 | var type = GetType(m_TestAssembly, "ShouldPatch.ClassWithNestedTypes/InternalNested"); 143 | MockInjector.IsPatched(type).ShouldBeTrue(); 144 | } 145 | 146 | [Test] 147 | public void Injection_IsConsistentForAllTypes() 148 | { 149 | // whatever the reasons are for a given type getting patched or not, we want it to be internally consistent 150 | foreach (var type in SelectTypes(m_TestAssembly, IncludeNested.Yes)) 151 | { 152 | var mockStaticField = type.Fields.SingleOrDefault(f => f.Name == MockInjector.InjectedMockStaticDataName); 153 | var mockField = type.Fields.SingleOrDefault(f => f.Name == MockInjector.InjectedMockDataName); 154 | var mockCtor = type.GetConstructors().SingleOrDefault(c => c.Parameters.Count == 1 && c.Parameters[0].ParameterType.FullName == typeof(MockPlaceholderType).FullName); 155 | 156 | var count = (mockStaticField != null ? 1 : 0) + (mockField != null ? 1 : 0) + (mockCtor != null ? 1 : 0); 157 | count.ShouldBeOneOf(0, 3); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/NSubstitute.Elevated.Tests/NSubstitute.Elevated.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(Unity_DotNetTargetFrameworks) 5 | 6 | 7 | 8 | 2018.2.1 9 | 10 | 11 | 15.9.0 12 | 13 | 14 | 3.1.0 15 | 16 | 17 | 3.11.0 18 | 19 | 20 | 3.11.0 21 | 22 | 23 | 3.0.1 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/NSubstitute.Elevated.Tests/PeVerifyTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NSubstitute.Elevated.Tests.Utilities; 3 | using NSubstitute.Elevated.Weaver; 4 | using NUnit.Framework; 5 | using Shouldly; 6 | 7 | namespace NSubstitute.Elevated.Tests 8 | { 9 | public class PeVerifyTests : TestFileSystemFixture 10 | { 11 | [Test] 12 | public void ExePath_CanBeFound() 13 | { 14 | var pePath = PeVerify.ExePath; 15 | 16 | pePath.ShouldNotBeNull(); 17 | File.Exists(pePath).ShouldBeTrue(); 18 | } 19 | 20 | [Test] 21 | public void ValidDll_VerifiesClean() 22 | { 23 | var dllPath = typeof(TestAttribute).Assembly.Location; // pick nunit, why not 24 | 25 | File.Exists(dllPath).ShouldBeTrue(); 26 | Should.NotThrow(() => PeVerify.Verify(dllPath)); 27 | } 28 | 29 | [Test] 30 | public void MissingDll_Throws() 31 | { 32 | var badPath = GetType().Assembly.Location + ".xyzzy"; 33 | 34 | File.Exists(badPath).ShouldBeFalse(); 35 | Should.Throw(() => PeVerify.Verify(badPath)); 36 | } 37 | 38 | [Test] 39 | public void InvalidDll_Throws() 40 | { 41 | var path = BaseDir.Combine("test.txt").WriteAllText("this is definitely not a valid dll"); 42 | 43 | File.Exists(path).ShouldBeTrue(); 44 | Should.Throw(() => PeVerify.Verify(path)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/NSubstitute.Elevated.Tests/Utilities/PatchingFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CodeDom.Compiler; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Mono.Cecil; 6 | using NiceIO; 7 | using NSubstitute.Elevated.Weaver; 8 | using Shouldly; 9 | using Unity.Core; 10 | 11 | namespace NSubstitute.Elevated.Tests.Utilities 12 | { 13 | public abstract class PatchingFixture : TestFileSystemFixture 14 | { 15 | public NPath Compile(string testAssemblyName, string sourceCode, params string[] dependentAssemblyNames) 16 | { 17 | var testAssemblyPath = BaseDir.Combine(testAssemblyName + ".dll"); 18 | 19 | // set up to compile 20 | 21 | var compiler = new Microsoft.CSharp.CSharpCodeProvider(); 22 | var compilerArgs = new CompilerParameters 23 | { 24 | OutputAssembly = testAssemblyPath, 25 | IncludeDebugInformation = true, 26 | CompilerOptions = "/o- /debug+ /warn:0" 27 | }; 28 | compilerArgs.ReferencedAssemblies.Add(typeof(int).Assembly.Location); // mscorlib 29 | compilerArgs.ReferencedAssemblies.AddRange( 30 | dependentAssemblyNames.Select(n => BaseDir.Combine(n + ".dll").ToString())); 31 | 32 | // compile and handle errors 33 | 34 | var compilerResult = compiler.CompileAssemblyFromSource(compilerArgs, sourceCode); 35 | if (compilerResult.Errors.Count > 0) 36 | { 37 | var errorText = compilerResult.Errors 38 | .OfType() 39 | .Select(e => $"({e.Line},{e.Column}): error {e.ErrorNumber}: {e.ErrorText}") 40 | .Prepend("Compiler errors:") 41 | .StringJoin("\n"); 42 | throw new Exception(errorText); 43 | } 44 | 45 | testAssemblyPath.ShouldBe(new NPath(compilerResult.PathToAssembly)); 46 | 47 | PeVerify.Verify(testAssemblyPath); // sanity check on what the compiler generated 48 | 49 | return testAssemblyPath; 50 | } 51 | 52 | public TypeDefinition GetType(AssemblyDefinition testAssembly, string typeName) 53 | => testAssembly.MainModule.GetType(typeName); 54 | public IEnumerable SelectTypes(AssemblyDefinition testAssembly, IncludeNested includeNested) 55 | => testAssembly.SelectTypes(includeNested); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/NSubstitute.Elevated.Tests/Utilities/TestFileSystemFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NiceIO; 3 | using NUnit.Framework; 4 | 5 | namespace NSubstitute.Elevated.Tests.Utilities 6 | { 7 | public abstract class TestFileSystemFixture 8 | { 9 | protected NPath BaseDir { private set; get; } 10 | 11 | [OneTimeSetUp] 12 | public void InitFixture() 13 | { 14 | var testDir = new NPath(TestContext.CurrentContext.TestDirectory); 15 | BaseDir = testDir.Combine("testfs_" + GetType().Name); 16 | CreateTestFileSystem(); 17 | } 18 | 19 | [OneTimeTearDown] 20 | public void TearDownFixture() => DeleteTestFileSystem(); 21 | 22 | protected void CreateTestFileSystem() 23 | { 24 | DeleteTestFileSystem(); 25 | BaseDir.CreateDirectory(); 26 | } 27 | 28 | protected void DeleteTestFileSystem() => BaseDir.DeleteIfExists(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Support/DependentAssembly/DependentAssembly.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(Unity_CoreTargetFrameworks) 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/Support/DependentAssembly/DependentType.cs: -------------------------------------------------------------------------------- 1 | namespace DependentAssembly 2 | { 3 | public class DependentType {} 4 | } 5 | -------------------------------------------------------------------------------- /tests/Support/SystemUnderTest/BasicTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | #pragma warning disable 169 5 | // ReSharper disable ClassNeverInstantiated.Global 6 | // ReSharper disable InconsistentNaming 7 | // ReSharper disable MemberInitializerValueIgnored 8 | // ReSharper disable PublicConstructorInAbstractClass 9 | // ReSharper disable UnusedMember.Local 10 | // ReSharper disable UnusedMember.Global 11 | // ReSharper disable UnusedParameter.Local 12 | 13 | namespace SystemUnderTest 14 | { 15 | public interface IBasicInterface 16 | { 17 | int GetValue(); 18 | } 19 | 20 | public class ClassWithVirtuals 21 | { 22 | public virtual int GetValue() => 4; 23 | } 24 | 25 | public class ClassWithDefaultCtor 26 | { 27 | public ClassWithDefaultCtor() 28 | { 29 | Value = 123; 30 | } 31 | 32 | public int Value = 234; 33 | 34 | public void Dummy() {} 35 | } 36 | 37 | public class ClassWithNoDefaultCtor 38 | { 39 | public ClassWithNoDefaultCtor(string i) {} 40 | public ClassWithNoDefaultCtor(string i1, string i2) {} 41 | 42 | void Dummy() {} 43 | } 44 | 45 | public class ClassWithNoDefaultCtorNoMethods 46 | { 47 | public ClassWithNoDefaultCtorNoMethods(string i) {} 48 | public ClassWithNoDefaultCtorNoMethods(string i1, string i2) {} 49 | } 50 | 51 | public class ClassWithCtorICall 52 | { 53 | public ClassWithCtorICall() 54 | { 55 | DoICall(); 56 | } 57 | 58 | [MethodImpl(MethodImplOptions.InternalCall)] 59 | static extern void DoICall(); 60 | } 61 | 62 | public class ClassWithCtorThrow 63 | { 64 | public ClassWithCtorThrow() 65 | { 66 | throw new InvalidOperationException(); 67 | } 68 | 69 | void Dummy() {} 70 | } 71 | 72 | public class EmptyClass 73 | { 74 | } 75 | 76 | public class ClassWithDependency 77 | { 78 | public DependentAssembly.DependentType Dummy() 79 | { 80 | return new DependentAssembly.DependentType(); 81 | } 82 | } 83 | 84 | public class SimpleClass 85 | { 86 | public int Modified; 87 | 88 | public void VoidMethod() => ++Modified; 89 | public int ReturnMethod() => ++Modified; 90 | public void VoidMethodWithParam(int count) => Modified += count; 91 | public int ReturnMethodWithParam(int count) => Modified += count; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Support/SystemUnderTest/SystemUnderTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(Unity_CoreTargetFrameworks) 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------