Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
79 changes: 75 additions & 4 deletions .github/workflows/Release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:

permissions:
contents: write
id-token: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand All @@ -27,6 +28,9 @@ env:
BUILD_X64_SC: 'bld/x64/Text-Grab-Self-Contained'
BUILD_ARM64: 'bld/arm64'
BUILD_ARM64_SC: 'bld/arm64/Text-Grab-Self-Contained'
ARTIFACT_SIGNING_ENDPOINT: 'https://eus.codesigning.azure.net/'
ARTIFACT_SIGNING_ACCOUNT_NAME: 'JoeFinAppsSigningCerts'
ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME: 'JoeFinApps'

jobs:
build:
Expand Down Expand Up @@ -77,7 +81,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-x64
--self-contained false
--no-self-contained
-c Release
-v minimal
-o ${{ env.BUILD_X64 }}
Expand All @@ -91,7 +95,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-x64
--self-contained true
--self-contained
-c Release
-v minimal
-o ${{ env.BUILD_X64_SC }}
Expand All @@ -105,7 +109,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-arm64
--self-contained false
--no-self-contained
-c Release
-v minimal
-o ${{ env.BUILD_ARM64 }}
Expand All @@ -118,7 +122,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-arm64
--self-contained true
--self-contained
-c Release
-v minimal
-o ${{ env.BUILD_ARM64_SC }}
Expand All @@ -137,6 +141,73 @@ jobs:
Rename-Item "${{ env.BUILD_ARM64_SC }}/${{ env.PROJECT }}.exe" 'Text-Grab-arm64.exe'
}

- name: Validate Azure Trusted Signing configuration
shell: pwsh
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: |
$requiredSecrets = @{
AZURE_TENANT_ID = $env:AZURE_TENANT_ID
AZURE_CLIENT_ID = $env:AZURE_CLIENT_ID
AZURE_SUBSCRIPTION_ID = $env:AZURE_SUBSCRIPTION_ID
}

$missingSecrets = @(
$requiredSecrets.GetEnumerator() |
Where-Object { [string]::IsNullOrWhiteSpace($_.Value) } |
ForEach-Object { $_.Key }
)

if ($missingSecrets.Count -gt 0) {
throw "Configure these repository secrets before running the release workflow: $($missingSecrets -join ', ')"
}

$signingConfig = @{
ARTIFACT_SIGNING_ENDPOINT = '${{ env.ARTIFACT_SIGNING_ENDPOINT }}'
ARTIFACT_SIGNING_ACCOUNT_NAME = '${{ env.ARTIFACT_SIGNING_ACCOUNT_NAME }}'
ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME = '${{ env.ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}'
}

$missing = @(
$signingConfig.GetEnumerator() |
Where-Object {
[string]::IsNullOrWhiteSpace($_.Value) -or
$_.Value.StartsWith('REPLACE_WITH_') -or
$_.Value.Contains('REPLACE_WITH_')
} |
ForEach-Object { $_.Key }
)

if ($missing.Count -gt 0) {
throw "Update the Azure Trusted Signing placeholders in .github/workflows/Release.yml before running the release workflow: $($missing -join ', ')"
}

- name: Azure login for Trusted Signing
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Sign release executables
uses: azure/artifact-signing-action@v1
with:
endpoint: ${{ env.ARTIFACT_SIGNING_ENDPOINT }}
signing-account-name: ${{ env.ARTIFACT_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ env.ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
files: |
${{ github.workspace }}\${{ env.BUILD_X64 }}\${{ env.PROJECT }}.exe
${{ github.workspace }}\${{ env.BUILD_X64_SC }}\${{ env.PROJECT }}.exe
${{ github.workspace }}\${{ env.BUILD_ARM64 }}\Text-Grab-arm64.exe
${{ github.workspace }}\${{ env.BUILD_ARM64_SC }}\Text-Grab-arm64.exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
description: Text Grab
description-url: https://github.com/TheJoeFin/Text-Grab

- name: Create self-contained archives
shell: pwsh
run: |
Expand Down
45 changes: 43 additions & 2 deletions Tests/CaptureLanguageUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
using Text_Grab.Interfaces;
using Text_Grab.Models;
using Text_Grab.Properties;
using Text_Grab.Utilities;

namespace Tests;

public class CaptureLanguageUtilitiesTests
[Collection("Settings isolation")]
public class CaptureLanguageUtilitiesTests : IDisposable
{
private readonly bool _originalUiAutomationEnabled;

public CaptureLanguageUtilitiesTests()
{
_originalUiAutomationEnabled = Settings.Default.UiAutomationEnabled;
}

public void Dispose()
{
Settings.Default.UiAutomationEnabled = _originalUiAutomationEnabled;
Settings.Default.Save();
LanguageUtilities.InvalidateAllCaches();
}

[Fact]
public void MatchesPersistedLanguage_MatchesByLanguageTag()
{
Expand All @@ -28,7 +45,7 @@ public void MatchesPersistedLanguage_MatchesLegacyTesseractDisplayName()
[Fact]
public void FindPreferredLanguageIndex_PrefersPersistedMatchBeforeFallbackLanguage()
{
List<Text_Grab.Interfaces.ILanguage> languages =
List<ILanguage> languages =
[
new UiAutomationLang(),
new WindowsAiLang(),
Expand All @@ -43,6 +60,30 @@ public void FindPreferredLanguageIndex_PrefersPersistedMatchBeforeFallbackLangua
Assert.Equal(0, index);
}

[WpfFact]
public async Task GetCaptureLanguagesAsync_ExcludesUiAutomationByDefault()
{
Settings.Default.UiAutomationEnabled = false;
Settings.Default.Save();
LanguageUtilities.InvalidateAllCaches();

List<ILanguage> languages = await CaptureLanguageUtilities.GetCaptureLanguagesAsync(includeTesseract: false);

Assert.DoesNotContain(languages, language => language is UiAutomationLang);
}

[WpfFact]
public async Task GetCaptureLanguagesAsync_IncludesUiAutomationWhenEnabled()
{
Settings.Default.UiAutomationEnabled = true;
Settings.Default.Save();
LanguageUtilities.InvalidateAllCaches();

List<ILanguage> languages = await CaptureLanguageUtilities.GetCaptureLanguagesAsync(includeTesseract: false);

Assert.Contains(languages, language => language is UiAutomationLang);
}

[Fact]
public void SupportsTableOutput_ReturnsFalseForUiAutomation()
{
Expand Down
30 changes: 30 additions & 0 deletions Tests/FilesIoTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Drawing;
using Text_Grab;
using Text_Grab.Models;
using Text_Grab.Utilities;

namespace Tests;
Expand All @@ -19,6 +20,35 @@ public async Task CanSaveImagesWithHistory()
Assert.True(couldSave);
}

[WpfFact]
public async Task SaveImageFile_SucceedsAfterClearTransientImage()
{
// Reproduces the race condition: SaveImageFile returns a Task that
// may still be running when ClearTransientImage nulls the bitmap.
// The save must complete successfully even when ClearTransientImage
// is called immediately after the fire-and-forget pattern used by
// HistoryService.SaveToHistory.
string path = FileUtilities.GetPathToLocalFile(fontSamplePath);
Bitmap bitmap = new(path);

HistoryInfo historyInfo = new()
{
ID = "save-race-test",
ImageContent = bitmap,
ImagePath = $"race_test_{Guid.NewGuid()}.bmp",
};

Task<bool> saveTask = FileUtilities.SaveImageFile(
historyInfo.ImageContent, historyInfo.ImagePath, FileStorageKind.WithHistory);

// Mirrors what HistoryService.SaveToHistory does right after the
// fire-and-forget call — must not cause saveTask to fail.
historyInfo.ClearTransientImage();

bool couldSave = await saveTask;
Assert.True(couldSave);
}

[WpfFact]
public async Task CanSaveTextFilesWithExe()
{
Expand Down
Loading
Loading