Skip to content

Commit bd1f6d8

Browse files
authored
feat: add HasAttribute and DoesNotHaveAttribute methods for IDirectoryInfo extensions (#150)
This pull request introduces support for verifying file attributes on directories in the `aweXpect.Testably` library, aligning the API for `IDirectoryInfo` with the existing support for `IFileInfo`. It also refactors attribute constraint logic for better code reuse, and adds comprehensive tests for the new functionality. New Features: * Added `HasAttribute` and `DoesNotHaveAttribute` extension methods for `IDirectoryInfo`, allowing assertions on directory attributes in a fluent style. * Updated the public API documentation to reflect the new methods for `IDirectoryInfo`. Refactoring and Code Reuse: * Moved the attribute constraint logic into a generic `FileSystemConstraints.HasAttributeConstraint<TInfo>` class, now used by both file and directory attribute assertions. This removes duplicate code and ensures consistent behavior. Testing: * Added unit tests for the new `HasAttribute` and `DoesNotHaveAttribute` methods on `IDirectoryInfo`, covering both success and failure scenarios as well as argument validation.
1 parent fb6937a commit bd1f6d8

8 files changed

Lines changed: 265 additions & 54 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Abstractions;
4+
using aweXpect.Core;
5+
using aweXpect.Core.Constraints;
6+
using aweXpect.Results;
7+
using aweXpect.Testably.Helpers;
8+
9+
namespace aweXpect.Testably;
10+
11+
public static partial class DirectoryInfoExtensions
12+
{
13+
/// <summary>
14+
/// Verifies that the <see cref="IDirectoryInfo" /> has the <paramref name="expected" />
15+
/// <see cref="FileAttributes" />.
16+
/// </summary>
17+
public static AndOrResult<IDirectoryInfo, IThat<IDirectoryInfo>> HasAttribute(this IThat<IDirectoryInfo> source,
18+
FileAttributes expected)
19+
{
20+
if (expected == default)
21+
{
22+
throw new ArgumentException(
23+
"The expected file attributes must include at least one flag.", nameof(expected));
24+
}
25+
26+
return new AndOrResult<IDirectoryInfo, IThat<IDirectoryInfo>>(
27+
source.Get().ExpectationBuilder.AddConstraint((it, grammars)
28+
=> new FileSystemConstraints.HasAttributeConstraint<IDirectoryInfo>(it, grammars, expected)),
29+
source);
30+
}
31+
32+
/// <summary>
33+
/// Verifies that the <see cref="IDirectoryInfo" /> does not have the <paramref name="unexpected" />
34+
/// <see cref="FileAttributes" />.
35+
/// </summary>
36+
public static AndOrResult<IDirectoryInfo, IThat<IDirectoryInfo>> DoesNotHaveAttribute(
37+
this IThat<IDirectoryInfo> source,
38+
FileAttributes unexpected)
39+
{
40+
if (unexpected == default)
41+
{
42+
throw new ArgumentException(
43+
"The unexpected file attributes must include at least one flag.", nameof(unexpected));
44+
}
45+
46+
return new AndOrResult<IDirectoryInfo, IThat<IDirectoryInfo>>(
47+
source.Get().ExpectationBuilder.AddConstraint((it, grammars)
48+
=> new FileSystemConstraints.HasAttributeConstraint<IDirectoryInfo>(it, grammars, unexpected).Invert()),
49+
source);
50+
}
51+
}
Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.IO;
33
using System.IO.Abstractions;
4-
using System.Text;
54
using aweXpect.Core;
65
using aweXpect.Core.Constraints;
76
using aweXpect.Results;
@@ -25,7 +24,7 @@ public static AndOrResult<IFileInfo, IThat<IFileInfo>> HasAttribute(this IThat<I
2524

2625
return new AndOrResult<IFileInfo, IThat<IFileInfo>>(
2726
source.Get().ExpectationBuilder.AddConstraint((it, grammars)
28-
=> new HasAttributeConstraint(it, grammars, expected)),
27+
=> new FileSystemConstraints.HasAttributeConstraint<IFileInfo>(it, grammars, expected)),
2928
source);
3029
}
3130

@@ -44,58 +43,7 @@ public static AndOrResult<IFileInfo, IThat<IFileInfo>> DoesNotHaveAttribute(this
4443

4544
return new AndOrResult<IFileInfo, IThat<IFileInfo>>(
4645
source.Get().ExpectationBuilder.AddConstraint((it, grammars)
47-
=> new HasAttributeConstraint(it, grammars, unexpected).Invert()),
46+
=> new FileSystemConstraints.HasAttributeConstraint<IFileInfo>(it, grammars, unexpected).Invert()),
4847
source);
4948
}
50-
51-
private sealed class HasAttributeConstraint(string it, ExpectationGrammars grammars, FileAttributes expected)
52-
: ConstraintResult.WithValue<IFileInfo>(grammars),
53-
IValueConstraint<IFileInfo>
54-
{
55-
private FileAttributes _actualAttributes;
56-
57-
public ConstraintResult IsMetBy(IFileInfo actual)
58-
{
59-
Actual = actual;
60-
if (!actual.Exists)
61-
{
62-
Outcome = Outcome.Failure;
63-
return this;
64-
}
65-
66-
_actualAttributes = actual.Attributes;
67-
Outcome = (_actualAttributes & expected) == expected ? Outcome.Success : Outcome.Failure;
68-
return this;
69-
}
70-
71-
protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
72-
=> stringBuilder.Append("has attribute ").Append(expected);
73-
74-
protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
75-
{
76-
if (Actual?.Exists != true)
77-
{
78-
stringBuilder.Append(it).Append(" did not exist");
79-
}
80-
else
81-
{
82-
stringBuilder.Append(it).Append(" was ").Append(_actualAttributes);
83-
}
84-
}
85-
86-
protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
87-
=> stringBuilder.Append("does not have attribute ").Append(expected);
88-
89-
protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
90-
{
91-
if (Actual?.Exists != true)
92-
{
93-
stringBuilder.Append(it).Append(" did not exist");
94-
}
95-
else
96-
{
97-
stringBuilder.Append(it).Append(" did");
98-
}
99-
}
100-
}
10149
}

Source/aweXpect.Testably/Helpers/FileSystemConstraints.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23
using System.IO.Abstractions;
34
using System.Text;
45
using System.Threading;
@@ -11,6 +12,61 @@ namespace aweXpect.Testably.Helpers;
1112

1213
internal static class FileSystemConstraints
1314
{
15+
internal sealed class HasAttributeConstraint<TInfo>(
16+
string it,
17+
ExpectationGrammars grammars,
18+
FileAttributes expected)
19+
: ConstraintResult.WithValue<TInfo>(grammars),
20+
IValueConstraint<TInfo>
21+
where TInfo : class, IFileSystemInfo
22+
{
23+
private FileAttributes _actualAttributes;
24+
25+
public ConstraintResult IsMetBy(TInfo actual)
26+
{
27+
Actual = actual;
28+
if (!actual.Exists)
29+
{
30+
Outcome = Outcome.Failure;
31+
return this;
32+
}
33+
34+
_actualAttributes = actual.Attributes;
35+
Outcome = (_actualAttributes & expected) == expected ? Outcome.Success : Outcome.Failure;
36+
return this;
37+
}
38+
39+
protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
40+
=> stringBuilder.Append("has attribute ").Append(expected);
41+
42+
protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
43+
{
44+
if (Actual?.Exists != true)
45+
{
46+
stringBuilder.Append(it).Append(" did not exist");
47+
}
48+
else
49+
{
50+
stringBuilder.Append(it).Append(" was ").Append(_actualAttributes);
51+
}
52+
}
53+
54+
protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
55+
=> stringBuilder.Append("does not have attribute ").Append(expected);
56+
57+
protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
58+
{
59+
if (Actual?.Exists != true)
60+
{
61+
stringBuilder.Append(it).Append(" did not exist");
62+
}
63+
else
64+
{
65+
stringBuilder.Append(it).Append(" did");
66+
}
67+
}
68+
}
69+
1470
internal sealed class ExistsConstraint<TInfo>(string it, ExpectationGrammars grammars)
1571
: ConstraintResult.WithValue<TInfo>(grammars),
1672
IValueConstraint<TInfo>

Tests/aweXpect.Testably.Api.Tests/Expected/aweXpect.Testably_net10.0.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ namespace aweXpect.Testably
2828
public static class DirectoryInfoExtensions
2929
{
3030
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotExist(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source) { }
31+
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveAttribute(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.IO.FileAttributes unexpected) { }
3132
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveDirectory(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3233
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveFile(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3334
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> Exists(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source) { }
35+
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> HasAttribute(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.IO.FileAttributes expected) { }
3436
public static aweXpect.Results.TimeToleranceResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> HasCreationTime(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.DateTime expected) { }
3537
public static aweXpect.Testably.Results.DirectoryResult<System.IO.Abstractions.IDirectoryInfo> HasDirectory(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3638
public static aweXpect.Testably.Results.FileResult<System.IO.Abstractions.IDirectoryInfo> HasFile(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }

Tests/aweXpect.Testably.Api.Tests/Expected/aweXpect.Testably_net8.0.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ namespace aweXpect.Testably
2828
public static class DirectoryInfoExtensions
2929
{
3030
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotExist(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source) { }
31+
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveAttribute(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.IO.FileAttributes unexpected) { }
3132
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveDirectory(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3233
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveFile(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3334
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> Exists(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source) { }
35+
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> HasAttribute(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.IO.FileAttributes expected) { }
3436
public static aweXpect.Results.TimeToleranceResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> HasCreationTime(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.DateTime expected) { }
3537
public static aweXpect.Testably.Results.DirectoryResult<System.IO.Abstractions.IDirectoryInfo> HasDirectory(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3638
public static aweXpect.Testably.Results.FileResult<System.IO.Abstractions.IDirectoryInfo> HasFile(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }

Tests/aweXpect.Testably.Api.Tests/Expected/aweXpect.Testably_netstandard2.0.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ namespace aweXpect.Testably
2828
public static class DirectoryInfoExtensions
2929
{
3030
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotExist(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source) { }
31+
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveAttribute(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.IO.FileAttributes unexpected) { }
3132
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveDirectory(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3233
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> DoesNotHaveFile(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3334
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> Exists(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source) { }
35+
public static aweXpect.Results.AndOrResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> HasAttribute(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.IO.FileAttributes expected) { }
3436
public static aweXpect.Results.TimeToleranceResult<System.IO.Abstractions.IDirectoryInfo, aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo>> HasCreationTime(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> source, System.DateTime expected) { }
3537
public static aweXpect.Testably.Results.DirectoryResult<System.IO.Abstractions.IDirectoryInfo> HasDirectory(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
3638
public static aweXpect.Testably.Results.FileResult<System.IO.Abstractions.IDirectoryInfo> HasFile(this aweXpect.Core.IThat<System.IO.Abstractions.IDirectoryInfo> subject, string path) { }
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.IO;
2+
using System.IO.Abstractions;
3+
using Testably.Abstractions.Testing;
4+
5+
namespace aweXpect.Testably.Tests;
6+
7+
public sealed partial class DirectoryInfo
8+
{
9+
public sealed class DoesNotHaveAttribute
10+
{
11+
public sealed class Tests
12+
{
13+
[Fact]
14+
public async Task WhenAttributeIsAbsent_ShouldSucceed()
15+
{
16+
MockFileSystem fileSystem = new();
17+
string path = "foo";
18+
fileSystem.Directory.CreateDirectory(path);
19+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New(path);
20+
21+
async Task Act()
22+
{
23+
await That(dirInfo).DoesNotHaveAttribute(FileAttributes.ReadOnly);
24+
}
25+
26+
await That(Act).DoesNotThrow();
27+
}
28+
29+
[Fact]
30+
public async Task WhenAttributeIsPresent_ShouldFail()
31+
{
32+
MockFileSystem fileSystem = new();
33+
string path = "foo";
34+
fileSystem.Directory.CreateDirectory(path);
35+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New(path);
36+
37+
async Task Act()
38+
{
39+
await That(dirInfo).DoesNotHaveAttribute(FileAttributes.Directory);
40+
}
41+
42+
await That(Act).ThrowsException()
43+
.WithMessage("""
44+
Expected that dirInfo
45+
does not have attribute Directory,
46+
but it did
47+
""");
48+
}
49+
50+
[Fact]
51+
public async Task WhenDirectoryDoesNotExist_ShouldSucceed()
52+
{
53+
MockFileSystem fileSystem = new();
54+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New("foo");
55+
56+
async Task Act()
57+
{
58+
await That(dirInfo).DoesNotHaveAttribute(FileAttributes.Directory);
59+
}
60+
61+
await That(Act).DoesNotThrow();
62+
}
63+
64+
[Fact]
65+
public async Task WhenUnexpectedIsDefault_ShouldThrowArgumentException()
66+
{
67+
MockFileSystem fileSystem = new();
68+
string path = "foo";
69+
fileSystem.Directory.CreateDirectory(path);
70+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New(path);
71+
72+
async Task Act()
73+
{
74+
await That(dirInfo).DoesNotHaveAttribute(default);
75+
}
76+
77+
await That(Act).Throws<ArgumentException>()
78+
.WithParamName("unexpected");
79+
}
80+
}
81+
}
82+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.IO;
2+
using System.IO.Abstractions;
3+
using Testably.Abstractions.Testing;
4+
5+
namespace aweXpect.Testably.Tests;
6+
7+
public sealed partial class DirectoryInfo
8+
{
9+
public sealed class HasAttribute
10+
{
11+
public sealed class Tests
12+
{
13+
[Fact]
14+
public async Task WhenAttributeIsAbsent_ShouldFail()
15+
{
16+
MockFileSystem fileSystem = new();
17+
string path = "foo";
18+
fileSystem.Directory.CreateDirectory(path);
19+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New(path);
20+
21+
async Task Act()
22+
{
23+
await That(dirInfo).HasAttribute(FileAttributes.ReadOnly);
24+
}
25+
26+
await That(Act).ThrowsException()
27+
.WithMessage("""
28+
Expected that dirInfo
29+
has attribute ReadOnly,
30+
but it was Directory
31+
""");
32+
}
33+
34+
[Fact]
35+
public async Task WhenAttributeIsPresent_ShouldSucceed()
36+
{
37+
MockFileSystem fileSystem = new();
38+
string path = "foo";
39+
fileSystem.Directory.CreateDirectory(path);
40+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New(path);
41+
42+
async Task Act()
43+
{
44+
await That(dirInfo).HasAttribute(FileAttributes.Directory);
45+
}
46+
47+
await That(Act).DoesNotThrow();
48+
}
49+
50+
[Fact]
51+
public async Task WhenExpectedIsDefault_ShouldThrowArgumentException()
52+
{
53+
MockFileSystem fileSystem = new();
54+
string path = "foo";
55+
fileSystem.Directory.CreateDirectory(path);
56+
IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New(path);
57+
58+
async Task Act()
59+
{
60+
await That(dirInfo).HasAttribute(default);
61+
}
62+
63+
await That(Act).Throws<ArgumentException>()
64+
.WithParamName("expected");
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)