Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# README screenshot

The README embeds the root `screenshot.png`. Refresh it when requested or when visible main-window changes make the image stale (for example, picker layout, toolbar, comparison tree, or diff display). Screenshots intentionally omit the entire build-version label; build-label changes alone do not require a refresh.

On Windows, with PowerShell 7.2 or newer, a desktop session, and an SDK compatible with `global.json` (a stable .NET 10.0 SDK at or above `10.0.402`), run from the repository root:

```powershell
pwsh -NoProfile -File .\scripts\Update-ReadmeScreenshot.ps1
```

The command builds the current code and captures the real form with fixed fictional data. It does not connect to Kusto or read/write saved user settings. Keep those guarantees: never substitute real credentials, connections, schemas, or a full-desktop capture. No manual app setup is needed.

Inspect the generated image before including `screenshot.png` with the relevant UI changes. Confirm the build-version label is absent and both pickers, expanded comparison, checked `CurrentTime` function, and red/yellow diff are visible. If capture is unavailable or fails, report that limitation rather than fabricating an image or claiming it was refreshed. Do not edit the image by hand or add automatic CI updates.

See `CONTRIBUTING.md` for prerequisites and capture details.
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ The CI regression tests run offline; they do not validate live Kusto authenticat

The main window shows a date-based build version in `Build YYYYMMDD` format (currently `Build 20261020`, for October 20, 2026). To update the displayed date, change `label2.Text` in `SyncKusto\MainForm.Designer.cs`.

### Refreshing the README screenshot

On Windows, install PowerShell 7.2 or newer and an SDK compatible with `global.json`, then run:

```powershell
pwsh -NoProfile -File .\scripts\Update-ReadmeScreenshot.ps1
```

The script resolves the repository from its own location, so it also works when invoked by absolute path from another working directory. It performs a locked restore using your normal trusted NuGet configuration, builds the application and test project in Release, and runs only the explicitly selected screenshot fixture. Package restore may require network access; the capture itself is completely offline.

The fixture opens the real main form without requesting activation, with in-memory settings and a fake presenter supplying a fictional modified `CurrentTime` function. It never uses live Kusto connections, credentials, saved settings, or schema files. Windows renders only that window, including its native rich-text diff; no desktop pixels or unrelated windows are captured. A Windows desktop session is required; this is not a browser or headless cross-platform renderer.

Screenshots intentionally omit the entire build-version label, regardless of its value. This is limited to the capture fixture and does not change the production UI. Build-label changes alone do not require refreshing the screenshot.

The command replaces root `screenshot.png` only after capture succeeds and the PNG passes content and size checks. Capture has a timeout, and failures leave the existing image unchanged. Normal test runs do not regenerate the image, and no CI or scheduled regeneration is configured.

Inspect the result before including it with your changes: the build-version label should be absent, and the source and target pickers, expanded comparison, checked function, and red/yellow diff should be visible. The demo content and window dimensions are fixed, but native window chrome, fonts, and theme can vary between Windows environments; byte-identical output across machines is not guaranteed. Agents should follow the refresh guidance in `AGENTS.md`.

### Dependency restore policy

Local development and CI deliberately select sources differently:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ The Sync Kusto tool was built to help create a maintainable development process

![Screenshot](/screenshot.png)

To regenerate this image automatically, see [Refreshing the README screenshot](CONTRIBUTING.md#refreshing-the-readme-screenshot).

## Scope
This tool is intended specifically for the developer desktop scenario. It will not help if you need to build an automated syncing process, generate change scripts, sync policies, etc. For all of that, we recommend using [Delta Kusto](https://github.com/microsoft/delta-kusto).

Expand Down
151 changes: 151 additions & 0 deletions SyncKusto.Tests/Screenshots/ReadmeScreenshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Kusto.Data.Common;
using Moq;
using NUnit.Framework;
using SyncKusto.Abstractions;
using SyncKusto.Core.Abstractions;
using SyncKusto.Core.Models;
using SyncKusto.Core.Services;
using SyncKusto.ErrorHandling;
using SyncKusto.Kusto.Models;
using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection;
using System.Runtime.InteropServices;
using static SyncKusto.Tests.Forms.FormsTestHelper;

namespace SyncKusto.Tests.Screenshots;

[TestFixture]
[Apartment(ApartmentState.STA)]
[NonParallelizable]
public class ReadmeScreenshotTests
{
[Test]
[Explicit("Run scripts\\Update-ReadmeScreenshot.ps1 to refresh the README image.")]
public void Capture()
{
string? outputPath = Environment.GetEnvironmentVariable("SYNCKUSTO_SCREENSHOT_PATH");
Assert.That(outputPath, Is.Not.Null.And.Not.Empty, "Use scripts\\Update-ReadmeScreenshot.ps1.");
Assert.That(Environment.UserInteractive, Is.True, "Screenshot capture requires a Windows desktop session.");

Application.SetHighDpiMode(HighDpiMode.DpiUnaware);
Application.SetDefaultFont(new Font("Microsoft Sans Serif", 8.25f));
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

var source = new DatabaseSchema("demo-source", "");
var target = new DatabaseSchema("demo-target", "");
var current = CreateFunction("{\r\n print now(), Yesterday=ago(1d)\r\n}");
source.Functions.Add(current.Name, current);
target.Functions.Add(current.Name, CreateFunction("{\r\n print now()\r\n}"));
var comparison = new ComparisonResult(
new SchemaDifferenceResult(Array.Empty<SchemaDifference>(),
new[] { new FunctionSchemaDifference(new Modified(), new KustoFunctionSchema(current)) }),
source, target);
var presenter = new Mock<IMainFormPresenter>(MockBehavior.Strict);
presenter.Setup(p => p.CompareAsync(It.IsAny<SchemaSourceInfo>(), It.IsAny<SchemaSourceInfo>(),
It.IsAny<IProgress<SyncProgress>>(), default)).ReturnsAsync(comparison);
var settings = new InMemorySettingsProvider();
var validation = new SchemaValidationService(settings);
var kustoValidation = new Mock<IKustoValidationService>(MockBehavior.Strict);
using var form = new CaptureForm(presenter.Object, settings, validation, kustoValidation.Object)
{
StartPosition = FormStartPosition.Manual,
Location = new Point(0, 0),
Size = new Size(655, 854),
ShowInTaskbar = false
};
var sourcePicker = form.MainField<SchemaPickerControl>("spcSource");
Field<TextBox>(sourcePicker, "txtFilePath").Text = @"C:\Demo\Weather";
var targetPicker = form.MainField<SchemaPickerControl>("spcTarget");
Field<RadioButton>(targetPicker, "rbKusto").Checked = true;
Field<ComboBox>(targetPicker, "cbCluster").Text = "https://demo.kusto.windows.net";
Field<ComboBox>(targetPicker, "cbDatabase").Text = "Weather";
form.MainField<Label>("label2").Visible = false;
form.Show();

var compare = form.CompareSchemasAsync(sourcePicker.GetSourceInfo(), targetPicker.GetSourceInfo());
PumpUntil(() => compare.IsCompleted);
compare.GetAwaiter().GetResult();
var tree = form.MainField<TreeView>("tvComparison");
tree.ExpandAll();
var functionNode = tree.Nodes[0].Nodes[0].Nodes[0];
functionNode.Checked = true;
tree.HideSelection = false;
tree.SelectedNode = functionNode;
typeof(MainForm).GetMethod("tvComparison_NodeMouseClick", BindingFlags.NonPublic | BindingFlags.Instance)!
.Invoke(form, new object[] { tree, new TreeNodeMouseClickEventArgs(functionNode, MouseButtons.Left, 1, 0, 0) });
var diff = form.MainField<RichTextBox>("rtbSourceText");
diff.SelectionStart = 0;
diff.SelectionLength = 0;
diff.ScrollToCaret();
form.Refresh();
Application.DoEvents();

Assert.That(form.Size, Is.EqualTo(new Size(655, 854)));
Assert.That(functionNode.Text, Is.EqualTo("CurrentTime"));
Assert.That(functionNode.Checked, Is.True);
Assert.That(diff.Text, Does.Contain("print now()").And.Contain("Yesterday=ago(1d)"));
Assert.That(form.MainField<ToolStripButton>("btnUpdate").Enabled, Is.True);
presenter.Verify(p => p.CompareAsync(It.IsAny<SchemaSourceInfo>(), It.IsAny<SchemaSourceInfo>(),
It.IsAny<IProgress<SyncProgress>>(), default), Times.Once);
presenter.VerifyNoOtherCalls();
kustoValidation.VerifyNoOtherCalls();

Assert.That(form.MainField<Label>("label2").Visible, Is.False, "Screenshots must omit the build-version label.");
using var bitmap = new Bitmap(form.Width, form.Height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
nint deviceContext = graphics.GetHdc();
try
{
Assert.That(PrintWindow(form.Handle, deviceContext, 0), Is.True, "Windows could not render the demo window.");
}
finally
{
graphics.ReleaseHdc(deviceContext);
}
}

// RichTextBox is not reliably rendered by Control.DrawToBitmap.
Assert.That(CountPixels(bitmap, Color.Red), Is.GreaterThan(500), "The deleted diff line was not captured.");
Assert.That(CountPixels(bitmap, Color.Yellow), Is.GreaterThan(500), "The inserted diff line was not captured.");
bitmap.Save(outputPath!, ImageFormat.Png);
using var saved = new Bitmap(outputPath!);
Assert.That(saved.Size, Is.EqualTo(bitmap.Size));
}

private static FunctionSchema CreateFunction(string body) =>
new("CurrentTime", new List<FunctionParameterSchema>(), body, "", "",
FunctionSchema.FunctionKind.Unknown, Array.Empty<ColumnSchema>());

private static int CountPixels(Bitmap bitmap, Color color)
{
int count = 0;
for (int y = 0; y < bitmap.Height; y++)
for (int x = 0; x < bitmap.Width; x++)
if (bitmap.GetPixel(x, y).ToArgb() == color.ToArgb())
count++;
return count;
}

private sealed class CaptureForm(
IMainFormPresenter presenter,
ISettingsProvider settings,
ISchemaValidationService validation,
IKustoValidationService kustoValidation)
: MainForm(presenter, ErrorMessageResolverFactory.CreateDefault(), settings, validation, kustoValidation)
{
protected override bool ShowWithoutActivation => true;

public T MainField<T>(string name) =>
(T)typeof(MainForm).GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(this)!;
}

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PrintWindow(nint window, nint deviceContext, uint flags);
}
Binary file modified screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions scripts/Update-ReadmeScreenshot.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#Requires -Version 7.2
<#
.SYNOPSIS
Builds SyncKusto and refreshes the README screenshot using fictional, offline data.
#>
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

if (-not $IsWindows) {
throw 'Screenshot capture requires Windows and a desktop session.'
}

$repoRoot = Split-Path -Parent $PSScriptRoot
$dotnet = (Get-Command dotnet -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
$temporaryDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ("synckusto-screenshot-" + [guid]::NewGuid().ToString('N'))
$stagingPath = Join-Path $repoRoot (".screenshot-" + [guid]::NewGuid().ToString('N') + '.png')

Push-Location $repoRoot
try {
$requiredSdk = (Get-Content -LiteralPath 'global.json' -Raw | ConvertFrom-Json).sdk.version
$actualSdk = & $dotnet --version
if ($LASTEXITCODE -ne 0) {
throw "Install a .NET SDK compatible with global.json ($requiredSdk or a newer servicing patch in the same feature band) and ensure dotnet can find it."
}
Write-Verbose "Using .NET SDK $actualSdk."

& $dotnet restore SyncKusto.sln --locked-mode --verbosity quiet
if ($LASTEXITCODE -ne 0) { throw 'Locked dependency restore failed. The existing screenshot was not changed.' }
& $dotnet build SyncKusto.Tests\SyncKusto.Tests.csproj --configuration Release --no-restore --verbosity quiet
if ($LASTEXITCODE -ne 0) { throw 'Build failed. The existing screenshot was not changed.' }

[System.IO.Directory]::CreateDirectory($temporaryDirectory) | Out-Null
$capturePath = Join-Path $temporaryDirectory 'screenshot.png'
$startInfo = [System.Diagnostics.ProcessStartInfo]::new($dotnet)
$startInfo.UseShellExecute = $false
$startInfo.WorkingDirectory = $repoRoot
foreach ($argument in @(
'test', 'SyncKusto.Tests\SyncKusto.Tests.csproj',
'--configuration', 'Release', '--no-build', '--no-restore',
'--filter', 'FullyQualifiedName=SyncKusto.Tests.Screenshots.ReadmeScreenshotTests.Capture',
'--results-directory', $temporaryDirectory,
'--', 'RunConfiguration.TreatNoTestsAsError=true', 'NUnit.ExplicitMode=Strict'
)) {
$startInfo.ArgumentList.Add($argument)
}
$startInfo.Environment['SYNCKUSTO_SCREENSHOT_PATH'] = $capturePath
$process = [System.Diagnostics.Process]::Start($startInfo)
try {
if (-not $process.WaitForExit(120000)) {
$process.Kill($true)
$process.WaitForExit()
throw 'Screenshot capture timed out. The existing screenshot was not changed.'
}
if ($process.ExitCode -ne 0) {
throw 'Screenshot capture failed. The existing screenshot was not changed.'
}
}
finally {
$process.Dispose()
}

$png = [System.IO.File]::ReadAllBytes($capturePath)
if ($png.Length -lt 24 -or
[Convert]::ToHexString($png[0..7]) -ne '89504E470D0A1A0A' -or
[System.Text.Encoding]::ASCII.GetString($png, 12, 4) -ne 'IHDR') {
throw 'Capture did not produce a valid PNG. The existing screenshot was not changed.'
}
$width = [Convert]::ToInt32([Convert]::ToHexString([byte[]]$png[16..19]), 16)
$height = [Convert]::ToInt32([Convert]::ToHexString([byte[]]$png[20..23]), 16)
if ($width -ne 655 -or $height -ne 854) {
throw "Unexpected screenshot dimensions: ${width}x${height}. The existing screenshot was not changed."
}

[System.IO.File]::Copy($capturePath, $stagingPath)
$destination = Join-Path $repoRoot 'screenshot.png'
if (Test-Path -LiteralPath $destination) {
[System.IO.File]::Replace($stagingPath, $destination, [NullString]::Value)
}
else {
[System.IO.File]::Move($stagingPath, $destination)
}
Write-Host "Updated $destination"
}
finally {
if (Test-Path -LiteralPath $stagingPath) {
Remove-Item -LiteralPath $stagingPath -Force
}
if (Test-Path -LiteralPath $temporaryDirectory) {
Remove-Item -LiteralPath $temporaryDirectory -Recurse -Force
}
Pop-Location
}
Loading