Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of a new DocAssembler tool #87

Merged
merged 14 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This repository contains a series of tools, templates, tips and tricks to make y

## Tools

* [DocAssembler 🆕](./src/DocAssembler): assemble documentation and assets from various locations on disk and assemble them in one place. It is possible to restructure, where the links are changed to the right location.
* [DocFxTocGenerator](./src/DocFxTocGenerator): generate a Table of Contents (TOC) in YAML format for DocFX. It has features like the ability to configure the order of files and the names of documents and folders.
* [DocLinkChecker](./src/DocLinkChecker): validate links in documents and check for orphaned attachments in the `.attachments` folder. The tool indicates whether there are errors or warnings, so it can be used in a CI pipeline. It can also clean up orphaned attachments automatically. And it can validate table syntax.
* [DocLanguageTranslator](./src/DocLanguageTranslator): allows to generate and translate automatically missing files or identify missing files in multi language pattern directories.
Expand Down Expand Up @@ -66,6 +67,7 @@ choco install docfx-companion-tools
You can as well install the tools through `dotnet tool`.

```shell
dotnet tool install DocAssembler -g
dotnet tool install DocFxTocGenerator -g
dotnet tool install DocLanguageTranslator -g
dotnet tool install DocLinkChecker -g
Expand Down
348 changes: 348 additions & 0 deletions src/DocAssembler/DocAssembler.Test/AssembleActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
// <copyright file="AssembleActionTests.cs" company="DocFx Companion Tools">
// Copyright (c) DocFx Companion Tools. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>
using Bogus;
using DocAssembler.Actions;
using DocAssembler.Configuration;
using DocAssembler.Test.Helpers;
using FluentAssertions;
using Microsoft.Extensions.Logging;

namespace DocAssembler.Test;

public class AssembleActionTests
{
private Faker _faker = new();
private MockFileService _fileService = new();
private MockLogger _mockLogger = new();
private ILogger _logger;

private string _workingFolder = string.Empty;
private string _outputFolder = string.Empty;

public AssembleActionTests()
{
_fileService.FillDemoSet();
_logger = _mockLogger.Logger;

_workingFolder = _fileService.Root;
_outputFolder = Path.Combine(_fileService.Root, "out");
}

[Fact]
public async void Run_MinimumConfigAllCopied()
{
// arrange
AssembleConfiguration config = new AssembleConfiguration
{
DestinationFolder = "out",
Content =
[
new Content
{
SourceFolder = ".docfx",
Files = { "**" },
RawCopy = true, // just copy the content
}
],
};

// all files in .docfx and docs-children
int count = _fileService.Files.Count;
var expected = _fileService.Files.Where(x => x.Key.Contains("/.docfx/"));

InventoryAction inventory = new(_workingFolder, config, _fileService, _logger);
await inventory.RunAsync();
AssembleAction action = new(config, inventory.Files, _fileService, _logger);

// act
var ret = await action.RunAsync();

// assert
ret.Should().Be(ReturnCode.Normal);
// expect files to be original count + expected files and folders + new "out" folder.
_fileService.Files.Should().HaveCount(count + expected.Count() + 1);

// validate file content is copied
var file = expected.Single(x => x.Key.EndsWith("index.md"));
var expectedPath = file.Key.Replace("/.docfx/", $"/{config.DestinationFolder}/");
var newFile = _fileService.Files.SingleOrDefault(x => x.Key == expectedPath);
newFile.Should().NotBeNull();
newFile.Value.Should().Be(file.Value);
}

[Fact]
public async void Run_MinimumConfigAllCopied_WithGlobalContentReplace()
{
// arrange
AssembleConfiguration config = new AssembleConfiguration
{
DestinationFolder = "out",
ContentReplacements =
[
new Replacement
{
Expression = @"(?<pre>[$\s])AB#(?<id>[0-9]{3,6})",
Value = @"${pre}[AB#${id}](https://dev.azure.com/MyCompany/MyProject/_workitems/edit/${id})"
}
],
Content =
[
new Content
{
SourceFolder = "docs",
DestinationFolder = "general",
Files = { "**" },
}
],
};

// all files in .docfx and docs-children
int count = _fileService.Files.Count;
var expected = _fileService.Files.Where(x => x.Key.StartsWith($"{_fileService.Root}/docs/"));

InventoryAction inventory = new(_workingFolder, config, _fileService, _logger);
await inventory.RunAsync();
AssembleAction action = new(config, inventory.Files, _fileService, _logger);

// act
var ret = await action.RunAsync();

// assert
ret.Should().Be(ReturnCode.Normal);
// expect files to be original count + expected files and folders + new "out" folder.
_fileService.Files.Should().HaveCount(count + expected.Count() + 1);

// validate file content is copied with changed AB# reference
var file = expected.Single(x => x.Key == $"{_fileService.Root}/docs/guidelines/documentation-guidelines.md");
var expectedPath = file.Key.Replace("/docs/", $"/{config.DestinationFolder}/general/");
var newFile = _fileService.Files.SingleOrDefault(x => x.Key == expectedPath);
newFile.Should().NotBeNull();
string[] lines = _fileService.ReadAllLines(newFile.Key);
var testLine = lines.Single(x => x.StartsWith("STANDARD:"));
testLine.Should().NotBeNull();
testLine.Should().Be("STANDARD: [AB#1234](https://dev.azure.com/MyCompany/MyProject/_workitems/edit/1234) reference");
}

[Fact]
public async void Run_MinimumConfigAllCopied_WithContentReplace()
{
// arrange
AssembleConfiguration config = new AssembleConfiguration
{
DestinationFolder = "out",
Content =
[
new Content
{
SourceFolder = "docs",
DestinationFolder = "general",
Files = { "**" },
ContentReplacements =
[
new Replacement
{
Expression = @"(?<pre>[$\s])AB#(?<id>[0-9]{3,6})",
Value = @"${pre}[AB#${id}](https://dev.azure.com/MyCompany/MyProject/_workitems/edit/${id})"
}
],
}
],
};

// all files in .docfx and docs-children
int count = _fileService.Files.Count;
var expected = _fileService.Files.Where(x => x.Key.StartsWith($"{_fileService.Root}/docs/"));

InventoryAction inventory = new(_workingFolder, config, _fileService, _logger);
await inventory.RunAsync();
AssembleAction action = new(config, inventory.Files, _fileService, _logger);

// act
var ret = await action.RunAsync();

// assert
ret.Should().Be(ReturnCode.Normal);
// expect files to be original count + expected files and folders + new "out" folder.
_fileService.Files.Should().HaveCount(count + expected.Count() + 1);

// validate file content is copied with changed AB# reference
var file = expected.Single(x => x.Key == $"{_fileService.Root}/docs/guidelines/documentation-guidelines.md");
var expectedPath = file.Key.Replace("/docs/", $"/{config.DestinationFolder}/general/");
var newFile = _fileService.Files.SingleOrDefault(x => x.Key == expectedPath);
newFile.Should().NotBeNull();
string[] lines = _fileService.ReadAllLines(newFile.Key);
var testLine = lines.Single(x => x.StartsWith("STANDARD:"));
testLine.Should().NotBeNull();
testLine.Should().Be("STANDARD: [AB#1234](https://dev.azure.com/MyCompany/MyProject/_workitems/edit/1234) reference");
}

[Fact]
public async void Run_MinimumConfigAllCopied_ContentShouldOverrideGlobal()
{
// arrange
AssembleConfiguration config = new AssembleConfiguration
{
DestinationFolder = "out",
ContentReplacements =
[
new Replacement
{
Expression = @"(?<pre>[$\s])AB#(?<id>[0-9]{3,6})",
Value = @"${pre}[AB#${id}](https://dev.azure.com/MyCompany/MyProject/_workitems/edit/${id})"
}
],
Content =
[
new Content
{
SourceFolder = "docs",
DestinationFolder = "general",
Files = { "**" },
ContentReplacements = [],
}
],
};

// all files in .docfx and docs-children
int count = _fileService.Files.Count;
var expected = _fileService.Files.Where(x => x.Key.StartsWith($"{_fileService.Root}/docs/"));

InventoryAction inventory = new(_workingFolder, config, _fileService, _logger);
await inventory.RunAsync();
AssembleAction action = new(config, inventory.Files, _fileService, _logger);

// act
var ret = await action.RunAsync();

// assert
ret.Should().Be(ReturnCode.Normal);
// expect files to be original count + expected files and folders + new "out" folder.
_fileService.Files.Should().HaveCount(count + expected.Count() + 1);

// validate file content is copied with changed AB# reference
var file = expected.Single(x => x.Key == $"{_fileService.Root}/docs/guidelines/documentation-guidelines.md");
var expectedPath = file.Key.Replace("/docs/", $"/{config.DestinationFolder}/general/");
var newFile = _fileService.Files.SingleOrDefault(x => x.Key == expectedPath);
newFile.Should().NotBeNull();
string[] lines = _fileService.ReadAllLines(newFile.Key);
var testLine = lines.Single(x => x.StartsWith("STANDARD:"));
testLine.Should().NotBeNull();
testLine.Should().Be("STANDARD: AB#1234 reference");
}

[Fact]
public async void Run_StandardConfigAllCopied()
{
// arrange
AssembleConfiguration config = GetStandardConfiguration();

// all files in .docfx and docs-children
int count = _fileService.Files.Count;
var expected = _fileService.Files.Where(x => x.Key.Contains("/.docfx/") ||
x.Key.Contains("/docs/"));

InventoryAction inventory = new(_workingFolder, config, _fileService, _logger);
await inventory.RunAsync();
AssembleAction action = new(config, inventory.Files, _fileService, _logger);

// act
var ret = await action.RunAsync();

// assert
ret.Should().Be(ReturnCode.Normal);
// expect files to be original count + expected files and folders + new "out" folder.
_fileService.Files.Should().HaveCount(count + expected.Count() + 1);

// validate file content is copied with changed AB# reference
var file = expected.Single(x => x.Key == $"{_fileService.Root}/docs/getting-started/README.md");
var expectedPath = file.Key.Replace("/docs/", $"/{config.DestinationFolder}/general/");
var newFile = _fileService.Files.SingleOrDefault(x => x.Key == expectedPath);
newFile.Should().NotBeNull();

string[] lines = _fileService.ReadAllLines(newFile.Key);

string testLine = lines.Single(x => x.StartsWith("EXTERNAL:"));
testLine.Should().Be("EXTERNAL: [.docassemble.json](https://github.com/example/blob/main/.docassemble.json)");

testLine = lines.Single(x => x.StartsWith("RESOURCE:"));
testLine.Should().Be("RESOURCE: ![computer](assets/computer.jpg)");

testLine = lines.Single(x => x.StartsWith("PARENT-DOC:"));
testLine.Should().Be("PARENT-DOC: [Docs readme](../README.md)");

testLine = lines.Single(x => x.StartsWith("RELATIVE-DOC:"));
testLine.Should().Be("RELATIVE-DOC: [Documentation guidelines](../guidelines/documentation-guidelines.md)");

testLine = lines.Single(x => x.StartsWith("ANOTHER-DOCS-TREE:"));
testLine.Should().Be("ANOTHER-DOCS-TREE: [System Copilot](../tools/system-copilot/README.md#usage)");

testLine = lines.Single(x => x.StartsWith("ANOTHER-DOCS-TREE-BACKSLASH:"));
testLine.Should().Be("ANOTHER-DOCS-TREE-BACKSLASH: [System Copilot](../tools/system-copilot/README.md#usage)");
}

private AssembleConfiguration GetStandardConfiguration()
{
return new AssembleConfiguration
{
DestinationFolder = "out",
ExternalFilePrefix = "https://github.com/example/blob/main/",
UrlReplacements =
[
new Replacement
{
Expression = @"/[Dd]ocs/",
Value = "/"
}
],
ContentReplacements =
[
new Replacement
{
Expression = @"(?<pre>[$\s])AB#(?<id>[0-9]{3,6})",
Value = @"${pre}[AB#${id}](https://dev.azure.com/MyCompany/MyProject/_workitems/edit/${id})"
},
new Replacement // Remove markdown style table of content
{
Expression = @"\[\[_TOC_\]\]",
Value = ""
}
],
Content =
[
new Content
{
SourceFolder = ".docfx",
Files = { "**" },
RawCopy = true, // just copy the content
UrlReplacements = [] // reset URL replacements
},
new Content
{
SourceFolder = "docs",
DestinationFolder = "general",
Files = { "**" },
},
new Content
{
SourceFolder = "shared", // part of general docs
DestinationFolder = "general/shared",
Files = { "**/docs/**" },
},
new Content
{
SourceFolder = "tools", // part of general docs
DestinationFolder = "general/tools",
Files = { "**/docs/**" },
},
new Content
{
SourceFolder = "backend",
DestinationFolder = "services", // change name to services
Files = { "**/docs/**" },
},
],
};
}
}
Loading
Loading