[duplicate-code] Duplicate low-level file-write method in HtmlReportEngine and CtrfReportEngine
type/tech-debttype/automation
## Summary
`HtmlReportEngine` and `CtrfReportEngine` each contain a private async method that performs the actual bytes-to-file write operation. The two methods have identical comments, the same `#if NETCOREAPP` branch structure, and the same `IFileStream` dispose pattern — only the method signatures differ (CTRF accepts a `FileMode` parameter; HTML hardcodes `FileMode.Create`). Meanwhile, `ReportFileWriterHelper` already exists in `SharedExtensionHelpers` as the intended home for shared file-writing helpers but does not yet cover this low-level write.
## Duplication Details
### Pattern: Identical `IFileStream` byte-write block with `#if NETCOREAPP` directive
- **Severity**: Low–Medium
- **Occurrences**: 2
- **Locations**:
- `src/Platform/Microsoft.Testing.Extensions.HtmlReport/HtmlReportEngine.cs` (lines 84–94, `WriteFileAsync`)
- `src/Platform/Microsoft.Testing.Extensions.CtrfReport/CtrfReportEngine.FileWriter.cs` (lines 80–89, `WriteAsync`)
- **Code Sample**:
```csharp
// HtmlReportEngine.WriteFileAsync (FileMode hardcoded to Create)
private async Task WriteFileAsync(string path, byte[] bytes)
{
// Note that we need to dispose the IFileStream, not the inner stream.
// IFileStream implementations will be responsible to dispose their inner stream.
using IFileStream stream = _fileSystem.NewFileStream(path, FileMode.Create);
#if NETCOREAPP
await stream.Stream.WriteAsync(bytes.AsMemory(), _cancellationToken).ConfigureAwait(false);
#else
await stream.Stream.WriteAsync(bytes, 0, bytes.Length, _cancellationToken).ConfigureAwait(false);
#endif
}
// CtrfReportEngine.WriteAsync (FileMode is a parameter)
private async Task WriteAsync(string path, FileMode mode, byte[] bytes)
{
// Note that we need to dispose the IFileStream, not the inner stream.
// IFileStream implementations will be responsible to dispose their inner stream.
using IFileStream stream = _fileSystem.NewFileStream(path, mode);
#if NETCOREAPP
await stream.Stream.WriteAsync(bytes.AsMemory(), _cancellationToken).ConfigureAwait(false);
#else
await stream.Stream.WriteAsync(bytes, 0, bytes.Length, _cancellationToken).ConfigureAwait(false);
#endif
}
```
## Impact Analysis
- **Maintainability**: Any platform-specific change to the write path (e.g., support for Span-based APIs on a new TFM) must be applied in two places. The identical comment block already signals copy-paste origin.
- **Bug Risk**: If a future TFM requires a different write strategy (e.g., `WriteAsync` overload deprecation), one engine could be updated while the other is missed.
- **Code Bloat**: The `ReportFileWriterHelper` class was specifically introduced for shared writing concerns but was not extended to cover this primitive — causing the drift.
## Refactoring Recommendations
1. **Add `WriteBytesAsync` to `ReportEngineBase`**
- Add a `protected Task WriteBytesAsync(string path, FileMode mode, byte[] bytes)` method to `src/Platform/SharedExtensionHelpers/ReportEngineBase.cs`.
- Both `HtmlReportEngine.WriteFileAsync` and `CtrfReportEngine.WriteAsync` delegate to this shared method.
- The shared implementation uses the `_fileSystem` and `_cancellationToken` already available on the base class.
- Estimated effort: 1 hour
- Benefits: Single `#if NETCOREAPP` branch to maintain; comment lives in one place.
2. **Alternatively, add a static helper to `ReportFileWriterHelper`**
- Add `internal static Task WriteBytesAsync(IFileSystem fileSystem, string path, FileMode mode, byte[] bytes, CancellationToken ct)` to `src/Platform/SharedExtensionHelpers/ReportFileWriterHelper.cs`.
- Both engines call the static helper.
- Estimated effort: 1 hour
- Benefits: Does not require touching the base class; keeps the helper class cohesive.
## Implementation Checklist
- [ ] Review duplication findings
- [ ] Choose refactoring approach (base class method vs. static helper)
- [ ] Implement shared write helper
- [ ] Replace both `WriteFileAsync`/`WriteAsync` bodies to delegate to the shared helper
- [ ] Verify all file-writing unit/acceptance tests still pass
## Analysis Metadata
- **Analyzed Files**: 2 source files + 1 shared helper
- **Detection Method**: Semantic code analysis — copy-paste with single-parameter variation
- **Commit**: 75c028427c3200217bd106bd885765d9c6048e5d
- **Analysis Date**: 2026-07-05
> 🤖 **Automated content by GitHub Copilot.** Posted via a maintainer's GitHub token, so it appears under their account — the account owner did **not** write or approve this content personally. Generated by the [Duplicate Code Detector](https://github.com/microsoft/testfx/actions/runs/28730689162/agentic_workflow) workflow. · 181 AIC · ⌖ 9.2 AIC · ⊞ 8K · [◷]( · [◷](https://github.com/search?q=repo%3Amicrosoft%2Ftestfx+is%3Aissue+%22gh-aw-workflow-call-id%3A+microsoft%2Ftestfx%2Fduplicate-code-detector%22&type=issues))
>
<details>
<summary>Add this agentic workflows to your repo</summary>
To install this agentic workflow, run
```
gh aw add githubnext/agentics/workflows/duplicate-code-detector.md@main
```
</details>
> - [x] expires <!-- gh-aw-expires: 2026-07-07T05:37:32.649Z --> on Jul 7, 2026, 5:37 AM UTC
<!-- gh-aw-agentic-workflow: Duplicate Code Detector, engine: copilot, version: 1.0.65, model: claude-sonnet-4.6, id: 28730689162, workflow_id: duplicate-code-detector, run: https://github.com/microsoft/testfx/actions/runs/28730689162 -->
<!-- gh-aw-workflow-id: duplicate-code-detector -->
<!-- gh-aw-workflow-call-id: microsoft/testfx/duplicate-code-detector -->
0 条评论