[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
// Copyright (c) Microsoft Corporation
|
|
|
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
|
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2026-06-15 10:32:18 +08:00
|
|
|
using System.IO;
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
using System.Linq;
|
|
|
|
|
|
2026-06-15 10:32:18 +08:00
|
|
|
using AdvancedPaste.Helpers;
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
using AdvancedPaste.Services.PythonScripts;
|
2026-06-15 10:32:18 +08:00
|
|
|
using AdvancedPaste.UnitTests.Mocks;
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
|
|
|
|
|
|
namespace AdvancedPaste.UnitTests.ServicesTests;
|
|
|
|
|
|
|
|
|
|
[TestClass]
|
|
|
|
|
public sealed class PythonScriptServiceTests
|
|
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
private PythonScriptService _service;
|
|
|
|
|
|
|
|
|
|
[TestInitialize]
|
|
|
|
|
public void Setup()
|
|
|
|
|
{
|
|
|
|
|
_service = new PythonScriptService(new IntegrationTestUserSettings());
|
|
|
|
|
}
|
|
|
|
|
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_DetectsSimpleImports()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"# @advancedpaste:name test",
|
|
|
|
|
"import requests",
|
|
|
|
|
"import numpy",
|
|
|
|
|
"import os",
|
|
|
|
|
"import sys",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(2, result.Count); // requests + numpy; os and sys are stdlib
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "requests" && r.PipPackage == "requests"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "numpy" && r.PipPackage == "numpy"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_DetectsFromImports()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"from PIL import Image",
|
|
|
|
|
"from markitdown import MarkItDown",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(2, result.Count);
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "PIL" && r.PipPackage == "Pillow"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "markitdown" && r.PipPackage == "markitdown"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_WellKnownMappings()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"import cv2",
|
|
|
|
|
"import win32clipboard",
|
|
|
|
|
"import yaml",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(3, result.Count);
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "cv2" && r.PipPackage == "opencv-python"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "win32clipboard" && r.PipPackage == "pywin32"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "yaml" && r.PipPackage == "PyYAML"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_ExplicitRequirementsTakePrecedence()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"import cv2",
|
|
|
|
|
"import requests",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var explicitReqs = new List<PythonRequirement>
|
|
|
|
|
{
|
|
|
|
|
new("cv2", "opencv-python-headless"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, explicitReqs);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(2, result.Count);
|
|
|
|
|
|
|
|
|
|
// cv2 should use the explicit pip package name, not the well-known mapping
|
|
|
|
|
var cv2Req = result.First(r => r.ImportName == "cv2");
|
|
|
|
|
Assert.AreEqual("opencv-python-headless", cv2Req.PipPackage);
|
|
|
|
|
|
|
|
|
|
// requests should be auto-detected
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "requests"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_SkipsStdlib()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"import os",
|
|
|
|
|
"import sys",
|
|
|
|
|
"import json",
|
|
|
|
|
"import io",
|
|
|
|
|
"import pathlib",
|
|
|
|
|
"import tempfile",
|
|
|
|
|
"import subprocess",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(0, result.Count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_SkipsComments()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"# import requests",
|
|
|
|
|
"# from PIL import Image",
|
|
|
|
|
"import json",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(0, result.Count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_HandlesMultipleImportsOnOneLine()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"import requests, numpy, pandas",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(3, result.Count);
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "requests"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "numpy"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "pandas"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void MergeWithAutoDetectedImports_HandlesSubmoduleImport()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"import win32com.client",
|
|
|
|
|
"from llama_cpp import Llama",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(2, result.Count);
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "win32com" && r.PipPackage == "pywin32"));
|
|
|
|
|
Assert.IsTrue(result.Any(r => r.ImportName == "llama_cpp" && r.PipPackage == "llama-cpp-python"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_ModuleNotFoundError()
|
|
|
|
|
{
|
|
|
|
|
var stderr = """
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
File "C:\scripts\reverse.py", line 4, in <module>
|
|
|
|
|
import win32clipboard
|
|
|
|
|
ModuleNotFoundError: No module named 'win32clipboard'
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
|
|
|
|
|
2026-04-15 16:10:49 +08:00
|
|
|
Assert.IsTrue(summary.Contains("reverse.py"), $"Summary should mention the script: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("line 4"), $"Summary should mention the line: {summary}");
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
Assert.IsTrue(summary.Contains("win32clipboard"), $"Summary should mention the module: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("pywin32"), $"Summary should suggest pip package: {summary}");
|
|
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_SyntaxError()
|
|
|
|
|
{
|
|
|
|
|
var stderr = """
|
|
|
|
|
File "test.py", line 5
|
|
|
|
|
def foo(
|
|
|
|
|
^
|
|
|
|
|
SyntaxError: unexpected EOF while parsing
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
|
|
|
|
|
2026-04-15 16:10:49 +08:00
|
|
|
Assert.IsTrue(summary.Contains("test.py"), $"Summary should mention the script: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("line 5"), $"Summary should mention the line: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("Python syntax error:"), $"Summary: {summary}");
|
|
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_SyntaxErrorWithColumn()
|
|
|
|
|
{
|
|
|
|
|
var stderr = " File \"script.py\", line 3\n x = (1 +\n ^\nSyntaxError: '(' was never closed\n";
|
|
|
|
|
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(summary.Contains("script.py"), $"Summary should mention the script: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("line 3"), $"Summary should mention the line: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("col"), $"Summary should mention the column: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("Python syntax error:"), $"Summary: {summary}");
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_GenericError()
|
|
|
|
|
{
|
|
|
|
|
var stderr = """
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
File "test.py", line 10, in <module>
|
|
|
|
|
result = 1 / 0
|
|
|
|
|
ZeroDivisionError: division by zero
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
|
|
|
|
|
2026-04-15 16:10:49 +08:00
|
|
|
Assert.IsTrue(summary.Contains("test.py"), $"Summary should mention the script: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("line 10"), $"Summary should mention the line: {summary}");
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
Assert.IsTrue(summary.Contains("ZeroDivisionError"), $"Summary: {summary}");
|
|
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 16:10:49 +08:00
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_NestedTraceback_ShowsLastFrame()
|
|
|
|
|
{
|
|
|
|
|
var stderr = """
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
File "main.py", line 5, in <module>
|
|
|
|
|
helper()
|
|
|
|
|
File "helper.py", line 12, in helper
|
|
|
|
|
do_work()
|
|
|
|
|
File "worker.py", line 8, in do_work
|
|
|
|
|
raise RuntimeError("bad state")
|
|
|
|
|
RuntimeError: bad state
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(summary.Contains("worker.py"), $"Summary should mention the last script in the chain: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("line 8"), $"Summary should mention the line of the last frame: {summary}");
|
|
|
|
|
Assert.IsTrue(summary.Contains("bad state"), $"Summary should contain the error message: {summary}");
|
|
|
|
|
}
|
|
|
|
|
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_EmptyStderr()
|
|
|
|
|
{
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(string.Empty);
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(summary));
|
|
|
|
|
Assert.AreEqual(string.Empty, details);
|
|
|
|
|
}
|
2026-04-15 16:10:49 +08:00
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePythonError_NoTraceback_PlainStderr()
|
|
|
|
|
{
|
|
|
|
|
var stderr = "Something went wrong in the script\n";
|
|
|
|
|
|
|
|
|
|
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
|
|
|
|
|
|
|
|
|
// No File "..." reference, so no location — just the message
|
|
|
|
|
Assert.IsTrue(summary.Contains("Something went wrong"), $"Summary: {summary}");
|
|
|
|
|
Assert.IsFalse(summary.Contains("line"), $"Summary should not contain 'line' without a traceback: {summary}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ExtractLastTracebackLocation_BasicTraceback()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"Traceback (most recent call last):",
|
|
|
|
|
" File \"script.py\", line 10, in <module>",
|
|
|
|
|
" result = 1 / 0",
|
|
|
|
|
"ZeroDivisionError: division by zero",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(location);
|
|
|
|
|
Assert.AreEqual("script.py", location.Value.FileName);
|
|
|
|
|
Assert.AreEqual(10, location.Value.Line);
|
|
|
|
|
Assert.IsNull(location.Value.Column);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ExtractLastTracebackLocation_WithCaret()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
" File \"test.py\", line 5",
|
|
|
|
|
" def foo(",
|
|
|
|
|
" ^",
|
|
|
|
|
"SyntaxError: unexpected EOF while parsing",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(location);
|
|
|
|
|
Assert.AreEqual("test.py", location.Value.FileName);
|
|
|
|
|
Assert.AreEqual(5, location.Value.Line);
|
|
|
|
|
Assert.IsNotNull(location.Value.Column);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ExtractLastTracebackLocation_FullPath_ReturnsBasename()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"Traceback (most recent call last):",
|
|
|
|
|
" File \"C:\\Users\\user\\scripts\\my_script.py\", line 42, in <module>",
|
|
|
|
|
" some_call()",
|
|
|
|
|
"ValueError: invalid value",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(location);
|
|
|
|
|
Assert.AreEqual("my_script.py", location.Value.FileName);
|
|
|
|
|
Assert.AreEqual(42, location.Value.Line);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ExtractLastTracebackLocation_NoFileLine_ReturnsNull()
|
|
|
|
|
{
|
|
|
|
|
var lines = new[]
|
|
|
|
|
{
|
|
|
|
|
"Some random error output",
|
|
|
|
|
"No traceback here",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
|
|
|
|
|
|
|
|
|
Assert.IsNull(location);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePipInstallError_ExtractsErrorLine()
|
|
|
|
|
{
|
|
|
|
|
var stderr = """
|
|
|
|
|
Collecting some-package
|
|
|
|
|
Downloading some-package-1.0.tar.gz (15 kB)
|
|
|
|
|
ERROR: Could not find a version that satisfies the requirement some-package (from versions: none)
|
|
|
|
|
ERROR: No matching distribution found for some-package
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
|
|
var (summary, fullStderr) = PythonScriptService.ParsePipInstallError(stderr);
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(summary.Contains("No matching distribution"), $"Summary should contain the last ERROR line: {summary}");
|
|
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(fullStderr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePipInstallError_NoErrorPrefix_UsesLastLine()
|
|
|
|
|
{
|
|
|
|
|
var stderr = "permission denied: /usr/lib/python3/dist-packages\n";
|
|
|
|
|
|
|
|
|
|
var (summary, fullStderr) = PythonScriptService.ParsePipInstallError(stderr);
|
|
|
|
|
|
|
|
|
|
Assert.IsTrue(summary.Contains("permission denied"), $"Summary: {summary}");
|
|
|
|
|
Assert.IsTrue(!string.IsNullOrEmpty(fullStderr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ParsePipInstallError_EmptyStderr()
|
|
|
|
|
{
|
|
|
|
|
var (summary, fullStderr) = PythonScriptService.ParsePipInstallError(string.Empty);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual("unknown error", summary);
|
|
|
|
|
Assert.AreEqual(string.Empty, fullStderr);
|
|
|
|
|
}
|
2026-05-11 10:44:12 +08:00
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_FunctionNameDeterminesFormat_Text()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
// The new interface uses function names like advanced_paste_from_text_to_text(...)
|
|
|
|
|
// to determine supported formats, not parameter signatures.
|
|
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_text(text):\n return text.upper()\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Text, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("text", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_FunctionNameDeterminesFormat_Html()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_html_to_text(html: str) -> str:\n return html\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Html, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("text", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_FunctionNameDeterminesFormat_Image()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_image_to_text(image_path):\n return 'desc'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Image, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("text", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_FunctionNameDeterminesFormat_Files()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_files_to_text(file_paths):\n return ''\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.File, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("text", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_OutputTypeHint_Image()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_image(text):\n return '/path/img.png'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual("image", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_OutputTypeHint_File()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_file(text):\n return '/path/out.txt'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual("file", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_OutputTypeHint_Files()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_files_to_files(file_paths):\n return file_paths\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual("files", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_RejectsMultipleFunctions()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript(
|
|
|
|
|
"def advanced_paste_from_text_to_text(text):\n return text\n\n" +
|
|
|
|
|
"def advanced_paste_from_html_to_text(html):\n return html\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNull(metadata);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_RejectsNoFunction()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var scriptPath = CreateTempScript("def some_other_function(text):\n return text\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNull(metadata);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
2026-06-15 10:32:18 +08:00
|
|
|
public void ReadMetadata_RejectsOldFormatWithoutTo()
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
// Old format (advanced_paste_from_text without _to_) should be rejected.
|
|
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_text(text):\n return text\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNull(metadata);
|
|
|
|
|
File.Delete(scriptPath);
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 16:26:40 +08:00
|
|
|
[TestMethod]
|
|
|
|
|
public void ReadMetadata_FunctionNameDeterminesFormat_Audio()
|
|
|
|
|
{
|
|
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_audio_to_text(audio_path):\n return 'transcribed'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Audio, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("text", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ReadMetadata_FunctionNameDeterminesFormat_Video()
|
|
|
|
|
{
|
|
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_video_to_text(video_path):\n return 'description'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Video, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("text", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ReadMetadata_OutputTypeHint_Audio()
|
|
|
|
|
{
|
|
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_audio(text):\n return '/path/out.mp3'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Text, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("audio", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TestMethod]
|
|
|
|
|
public void ReadMetadata_OutputTypeHint_Video()
|
|
|
|
|
{
|
|
|
|
|
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_video(text):\n return '/path/out.mp4'\n");
|
|
|
|
|
var metadata = _service.ReadMetadata(scriptPath);
|
|
|
|
|
|
|
|
|
|
Assert.IsNotNull(metadata);
|
|
|
|
|
Assert.AreEqual(Models.ClipboardFormat.Text, metadata.SupportedFormats);
|
|
|
|
|
Assert.AreEqual("video", metadata.OutputTypeHint);
|
|
|
|
|
File.Delete(scriptPath);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 10:32:18 +08:00
|
|
|
private static string CreateTempScript(string content)
|
2026-05-11 10:44:12 +08:00
|
|
|
{
|
2026-06-15 10:32:18 +08:00
|
|
|
var path = Path.Combine(Path.GetTempPath(), $"test_script_{Guid.NewGuid():N}.py");
|
|
|
|
|
File.WriteAllText(path, content);
|
|
|
|
|
return path;
|
2026-05-11 10:44:12 +08:00
|
|
|
}
|
[AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
- Complete reference for all @advancedpaste: header tags
- Windows and WSL/Linux execution mode protocols
- Declaring dependencies, security trust model, error handling
- Example scripts for both platforms
2. Custom scripts folder setting in Settings UI
- Added ScriptsFolder property to AdvancedPasteViewModel
- Added SettingsCard with TextBox + Browse folder dialog in XAML
- Added localization strings for the new setting
3. Auto-detect missing Python modules from import statements
- Scans script body for import/from-import statements
- Filters Python stdlib modules (CPython 3.12 set)
- Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
- Merges auto-detected imports with explicit @advancedpaste:requires entries
- Explicit requires always take precedence
4. Better error messages for Python script failures
- Parses stderr to extract the final Python exception line
- User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
- ModuleNotFoundError includes pip install hint from the mapping table
- Full traceback available in Details section of the error UI
Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
|
|
|
}
|