mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-29 03:41:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dafd6e067 | ||
|
|
e39fd0f839 | ||
|
|
e5af09d900 |
@@ -44,14 +44,6 @@ jobs:
|
|||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.artifact }}
|
name: ${{ inputs.artifact }}
|
||||||
# run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages
|
|
||||||
# beside them; the Windows arm64 build bundles none, so put one on PATH there.
|
|
||||||
- name: Install uv
|
|
||||||
if: runner.os == 'Windows' && runner.arch == 'ARM64'
|
|
||||||
uses: astral-sh/setup-uv@v10.2.0
|
|
||||||
with:
|
|
||||||
version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt
|
|
||||||
enable-cache: false
|
|
||||||
- uses: lukka/get-cmake@latest
|
- uses: lukka/get-cmake@latest
|
||||||
with:
|
with:
|
||||||
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
|
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
|
||||||
|
|||||||
+1
-3
@@ -52,6 +52,4 @@ internal_docs/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.opc
|
*.opc
|
||||||
/.test/
|
docs/superpowers/
|
||||||
docs/superpowers/
|
|
||||||
ctest_results.xml
|
|
||||||
+129
-30
@@ -214,9 +214,9 @@ function ShowModelInfo( pModel )
|
|||||||
|
|
||||||
SendWXDebugInfo("Model Name: "+sModelName);
|
SendWXDebugInfo("Model Name: "+sModelName);
|
||||||
|
|
||||||
$('#ModelName').html(sModelName);
|
$('#ModelName').text(sModelName);
|
||||||
$('#ModelName').attr('title',sModelName);
|
$('#ModelName').attr('title',sModelName);
|
||||||
$('#ModelAuthorName').html(sModelAuthor);
|
$('#ModelAuthorName').text(sModelAuthor);
|
||||||
|
|
||||||
switch(UploadType)
|
switch(UploadType)
|
||||||
{
|
{
|
||||||
@@ -268,7 +268,7 @@ function ShowModelInfo( pModel )
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#Model_Desc').html( html_decode(sModelDesc) );
|
$('#Model_Desc').empty().append( SanitizeDescHtml( html_decode(sModelDesc) ) );
|
||||||
|
|
||||||
let ModelPreviewList=pModel.preview_img;
|
let ModelPreviewList=pModel.preview_img;
|
||||||
let TotalPreview=ModelPreviewList.length;
|
let TotalPreview=ModelPreviewList.length;
|
||||||
@@ -281,16 +281,15 @@ function ShowModelInfo( pModel )
|
|||||||
|
|
||||||
if(TotalPreview>0)
|
if(TotalPreview>0)
|
||||||
{
|
{
|
||||||
let htmlPreview='';
|
$('#ModelPreviewList').empty();
|
||||||
for(let pn=0;pn<TotalPreview;pn++)
|
for(let pn=0;pn<TotalPreview;pn++)
|
||||||
{
|
{
|
||||||
//let FTmpPath=decodeURIComponent(ModelPreviewList[pn]);
|
//let FTmpPath=decodeURIComponent(ModelPreviewList[pn]);
|
||||||
let FTmpPath=ModelPreviewList[pn]['filepath'];
|
let FTmpPath=ModelPreviewList[pn]['filepath'];
|
||||||
|
|
||||||
htmlPreview+='<div class="swiper-slide"><img class="Model_PrevImg" src="'+FTmpPath+'" /></div>';
|
$('#ModelPreviewList').append( $('<div class="swiper-slide"></div>').append( $('<img class="Model_PrevImg" />').attr('src',FTmpPath) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#ModelPreviewList').html(htmlPreview);
|
|
||||||
$('#Model_Preview_Image').viewer({
|
$('#Model_Preview_Image').viewer({
|
||||||
title: false,
|
title: false,
|
||||||
fullsreen: false,
|
fullsreen: false,
|
||||||
@@ -410,7 +409,8 @@ function ConstructFileHtml( ID, pItem )
|
|||||||
{
|
{
|
||||||
let fTotal=pItem.length;
|
let fTotal=pItem.length;
|
||||||
|
|
||||||
let strHtml='';
|
let pBoard=$('#'+ID+' .FileListBoard');
|
||||||
|
pBoard.empty();
|
||||||
for( let f=0;f<fTotal;f++ )
|
for( let f=0;f<fTotal;f++ )
|
||||||
{
|
{
|
||||||
let pOne=pItem[f];
|
let pOne=pItem[f];
|
||||||
@@ -443,39 +443,139 @@ function ConstructFileHtml( ID, pItem )
|
|||||||
ImgPath='img/default.png';
|
ImgPath='img/default.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add html
|
//Add html. File names come from the 3MF, so build the nodes rather than concatenating markup.
|
||||||
|
let pIconImg=$('<img />').attr('src',ImgPath);
|
||||||
|
let pMenu=$('<div class="FileMenu"><img src="img/s.svg" /></div>');
|
||||||
if( strClass!='ImageIcon' )
|
if( strClass!='ImageIcon' )
|
||||||
{
|
{
|
||||||
strHtml+='<div class="FileItem">'+
|
pMenu.on('click', function(){ OnClickOpenFile(tPath); });
|
||||||
' <div class="'+strClass+'"><img src="'+ImgPath+'" /></div>'+
|
|
||||||
' <div class="FileText">'+
|
|
||||||
' <div class="FileName">'+tName+'</div>'+
|
|
||||||
' </div>'+
|
|
||||||
' <div class="FileMenu" onClick="OnClickOpenFile(\''+tPath+'\')"><img src="img/s.svg" /></div>'+
|
|
||||||
'</div>';
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImgID++;
|
ImgID++;
|
||||||
let TmpImgID="AF"+ImgID;
|
let TmpImgID="AF"+ImgID;
|
||||||
|
|
||||||
strHtml+='<div class="FileItem">'+
|
pIconImg.attr('id',TmpImgID);
|
||||||
' <div class="'+strClass+'"><img id="'+TmpImgID+'" src="'+ImgPath+'" /></div>'+
|
pMenu.on('click', function(){ OnClickOpenImage(TmpImgID); });
|
||||||
' <div class="FileText">'+
|
|
||||||
' <div class="FileName">'+tName+'</div>'+
|
|
||||||
' </div>'+
|
|
||||||
' <div class="FileMenu" onClick="OnClickOpenImage(\''+TmpImgID+'\')"><img src="img/s.svg" /></div>'+
|
|
||||||
'</div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pFileItem=$('<div class="FileItem"></div>');
|
||||||
|
pFileItem.append( $('<div></div>').addClass(strClass).append(pIconImg) );
|
||||||
|
pFileItem.append( $('<div class="FileText"></div>').append( $('<div class="FileName"></div>').text(tName).attr('title',tName) ) );
|
||||||
|
pFileItem.append( pMenu );
|
||||||
|
pBoard.append( pFileItem );
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#'+ID+' .FileListBoard').html(strHtml);
|
|
||||||
|
|
||||||
if( fTotal>0 )
|
if( fTotal>0 )
|
||||||
$('#'+ID).show();
|
$('#'+ID).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Descriptions are untrusted 3MF metadata that may carry rich-text HTML (e.g. from MakerWorld).
|
||||||
|
// Rebuild them from an inert parse, keeping only plain formatting tags and http(s) links and images.
|
||||||
|
var DescAllowedTags=['P','BR','B','STRONG','I','EM','U','S','STRIKE','DEL','INS','SUB','SUP','SMALL','MARK',
|
||||||
|
'Q','ABBR','KBD','WBR','H1','H2','H3','H4','H5','H6','UL','OL','LI','DL','DT','DD','BLOCKQUOTE','PRE','CODE',
|
||||||
|
'HR','SPAN','DIV','FIGURE','FIGCAPTION','TABLE','CAPTION','THEAD','TBODY','TFOOT','TR','TH','TD','A','IMG'];
|
||||||
|
// Plain attributes kept per tag; the numeric ones must be plain non-negative integers.
|
||||||
|
var DescAllowedAttrs={'IMG':['alt','title','width','height'],'TD':['colspan','rowspan'],'TH':['colspan','rowspan'],'OL':['start']};
|
||||||
|
var DescNumericAttrs=['width','height','colspan','rowspan','start'];
|
||||||
|
// Dropped together with their content; any other unknown tag is unwrapped to its children.
|
||||||
|
var DescDroppedTags=['SCRIPT','STYLE','TEMPLATE','NOSCRIPT','TEXTAREA','TITLE','IFRAME','FRAME','OBJECT','EMBED','SVG','MATH'];
|
||||||
|
|
||||||
|
function IsHttpUrl( strUrl )
|
||||||
|
{
|
||||||
|
// The scheme must be written as is, so nothing the URL parser would strip can precede or split it.
|
||||||
|
if( typeof strUrl!='string' || !/^https?:/i.test(strUrl) )
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
let sProtocol=new URL(strUrl).protocol;
|
||||||
|
return sProtocol=='http:' || sProtocol=='https:';
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Images load as soon as the page opens, so only https sources are kept: no plain-http requests to the local network.
|
||||||
|
function IsHttpsUrl( strUrl )
|
||||||
|
{
|
||||||
|
return IsHttpUrl(strUrl) && new URL(strUrl).protocol=='https:';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded YouTube players become a plain link to the video.
|
||||||
|
function GetYouTubeEmbedUrl( pNode )
|
||||||
|
{
|
||||||
|
let sSrc=pNode.getAttribute('src');
|
||||||
|
if( !IsHttpUrl(sSrc) )
|
||||||
|
return null;
|
||||||
|
let pUrl=new URL(sSrc);
|
||||||
|
return ( pUrl.origin=='https://www.youtube.com' && pUrl.pathname.indexOf('/embed/')==0 ) ? pUrl.href : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CopyDescNodes( pSrc, pDst )
|
||||||
|
{
|
||||||
|
for( let pNode=pSrc.firstChild;pNode!=null;pNode=pNode.nextSibling )
|
||||||
|
{
|
||||||
|
if( pNode.nodeType==Node.TEXT_NODE )
|
||||||
|
{
|
||||||
|
pDst.appendChild( document.createTextNode(pNode.nodeValue) );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if( pNode.nodeType!=Node.ELEMENT_NODE )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let sTag=pNode.nodeName.toUpperCase();
|
||||||
|
if( sTag=='IFRAME' )
|
||||||
|
{
|
||||||
|
let sVideoUrl=GetYouTubeEmbedUrl(pNode);
|
||||||
|
if( sVideoUrl!=null )
|
||||||
|
{
|
||||||
|
let pLink=document.createElement('A');
|
||||||
|
pLink.setAttribute('href',sVideoUrl);
|
||||||
|
pLink.textContent=sVideoUrl;
|
||||||
|
pDst.appendChild(pLink);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if( $.inArray(sTag,DescDroppedTags)>=0 )
|
||||||
|
continue;
|
||||||
|
if( $.inArray(sTag,DescAllowedTags)<0 )
|
||||||
|
{
|
||||||
|
CopyDescNodes(pNode,pDst);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pElem=document.createElement(sTag);
|
||||||
|
if( sTag=='A' && IsHttpUrl(pNode.getAttribute('href')) )
|
||||||
|
pElem.setAttribute('href',pNode.getAttribute('href'));
|
||||||
|
else if( sTag=='IMG' )
|
||||||
|
{
|
||||||
|
if( !IsHttpsUrl(pNode.getAttribute('src')) )
|
||||||
|
continue;
|
||||||
|
pElem.setAttribute('src',pNode.getAttribute('src'));
|
||||||
|
}
|
||||||
|
$.each( DescAllowedAttrs[sTag]||[], function(i,sAttr){
|
||||||
|
let sValue=pNode.getAttribute(sAttr);
|
||||||
|
if( sValue!=null && ( $.inArray(sAttr,DescNumericAttrs)<0 || /^\d+$/.test(sValue) ) )
|
||||||
|
pElem.setAttribute(sAttr,sValue);
|
||||||
|
});
|
||||||
|
CopyDescNodes(pNode,pElem);
|
||||||
|
pDst.appendChild(pElem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SanitizeDescHtml( strHtml )
|
||||||
|
{
|
||||||
|
let pFragment=document.createDocumentFragment();
|
||||||
|
// A DOMParser document is inert: it runs no scripts and loads no resources.
|
||||||
|
let pDoc=new DOMParser().parseFromString(strHtml,'text/html');
|
||||||
|
if( pDoc && pDoc.body )
|
||||||
|
CopyDescNodes(pDoc.body,pFragment);
|
||||||
|
return pFragment;
|
||||||
|
}
|
||||||
|
|
||||||
function ShowProfilelInfo( pProfile )
|
function ShowProfilelInfo( pProfile )
|
||||||
{
|
{
|
||||||
//==========Profile Info==========
|
//==========Profile Info==========
|
||||||
@@ -483,10 +583,10 @@ function ShowProfilelInfo( pProfile )
|
|||||||
let sProfileAuthor=decodeURIComponent(pProfile.author);
|
let sProfileAuthor=decodeURIComponent(pProfile.author);
|
||||||
let sProfileDesc=decodeURIComponent(pProfile.description);
|
let sProfileDesc=decodeURIComponent(pProfile.description);
|
||||||
|
|
||||||
$('#ProfileName').html(sProfileName);
|
$('#ProfileName').text(sProfileName);
|
||||||
$('#ProfileAuthor').html(sProfileAuthor);
|
$('#ProfileAuthor').text(sProfileAuthor);
|
||||||
|
|
||||||
$('#Profile_Desc').html( html_decode(sProfileDesc) );
|
$('#Profile_Desc').empty().append( SanitizeDescHtml( html_decode(sProfileDesc) ) );
|
||||||
|
|
||||||
let ProfilePreviewList=pProfile.preview_img;
|
let ProfilePreviewList=pProfile.preview_img;
|
||||||
let TotalPreview=ProfilePreviewList.length;
|
let TotalPreview=ProfilePreviewList.length;
|
||||||
@@ -499,15 +599,14 @@ function ShowProfilelInfo( pProfile )
|
|||||||
|
|
||||||
if(TotalPreview>0)
|
if(TotalPreview>0)
|
||||||
{
|
{
|
||||||
let htmlPreview='';
|
$('#ProfilePreviewList').empty();
|
||||||
for(let pn=0;pn<TotalPreview;pn++)
|
for(let pn=0;pn<TotalPreview;pn++)
|
||||||
{
|
{
|
||||||
let FTmpPath=ProfilePreviewList[pn]['filepath'];
|
let FTmpPath=ProfilePreviewList[pn]['filepath'];
|
||||||
|
|
||||||
htmlPreview+='<div class="swiper-slide"><img class="Model_PrevImg" src="'+FTmpPath+'" /></div>';
|
$('#ProfilePreviewList').append( $('<div class="swiper-slide"></div>').append( $('<img class="Model_PrevImg" />').attr('src',FTmpPath) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#ProfilePreviewList').html(htmlPreview);
|
|
||||||
$('#Profile_Preview_Image').viewer({
|
$('#Profile_Preview_Image').viewer({
|
||||||
title: false,
|
title: false,
|
||||||
fullsreen: false,
|
fullsreen: false,
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
# This file is made to support the unit tests workflow.
|
# This file is made to support the unit tests workflow.
|
||||||
# It should only require the directories build/tests, scripts/, and tests/ to function,
|
# It should only require the directories build/tests, scripts/, and tests/ to function,
|
||||||
# and cmake (with ctest) installed -- plus network access to PyPI whenever numpy has to
|
# and cmake (with ctest) installed.
|
||||||
# be installed into a freshly built test tree (see below).
|
|
||||||
# (otherwise, update the workflow too, but try to avoid to keep things self-contained)
|
# (otherwise, update the workflow too, but try to avoid to keep things self-contained)
|
||||||
#
|
#
|
||||||
# Usage: run_unit_tests.sh [TEST_DIR] [BUILD_CONFIG]
|
# Usage: run_unit_tests.sh [TEST_DIR] [BUILD_CONFIG]
|
||||||
@@ -19,52 +18,6 @@ cd "${ROOT_DIR}" || exit 1
|
|||||||
TEST_DIR="${1:-build/tests}"
|
TEST_DIR="${1:-build/tests}"
|
||||||
BUILD_CONFIG="${2:-}"
|
BUILD_CONFIG="${2:-}"
|
||||||
|
|
||||||
# The slic3rutils plugin-host tests build numpy arrays through the CPython copied next
|
|
||||||
# to the test binary (see tests/slic3rutils/CMakeLists.txt), which ships no numpy.
|
|
||||||
# Install it with the uv staged beside that runtime -- the tool the app installs plugin
|
|
||||||
# dependencies with -- straight into the interpreter's own site-packages: no pip needed
|
|
||||||
# in the runtime, no PYTHONPATH. Re-checked every run because a rebuild of the test
|
|
||||||
# target re-copies the runtime; needs network whenever it installs. Pinned so a numpy
|
|
||||||
# release cannot change results on its own.
|
|
||||||
NUMPY_VERSION="2.5.3"
|
|
||||||
|
|
||||||
# Without numpy those tests assert the numpy-absent error path instead, so a local run
|
|
||||||
# only warns. Under CI it fails the run, which would otherwise stay green while silently
|
|
||||||
# dropping the array coverage. (The Flatpak leg runs this inside `flatpak build`, whose
|
|
||||||
# minimal environment has no CI, and its offline build stages no uv, so numpy stays
|
|
||||||
# best-effort there.)
|
|
||||||
numpy_unavailable() {
|
|
||||||
if [ -n "${CI:-}" ]; then
|
|
||||||
echo "error: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "warning: $1; the numpy-backed binding tests will cover only the numpy-absent path."
|
|
||||||
}
|
|
||||||
|
|
||||||
has_pinned_numpy() {
|
|
||||||
"${python_exe}" -c "import sys, numpy; sys.exit(numpy.__version__ != '${NUMPY_VERSION}')" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_args=("${TEST_DIR}" \( -path '*/python/bin/python3' -o -path '*/python/python.exe' \))
|
|
||||||
# Multi-config trees hold one copy per configuration; only bootstrap the one being run.
|
|
||||||
[ -n "${BUILD_CONFIG}" ] && find_args+=(-path "*/${BUILD_CONFIG}/*")
|
|
||||||
python_exe="$(find "${find_args[@]}" -print -quit 2>/dev/null)"
|
|
||||||
|
|
||||||
if [ -z "${python_exe}" ]; then
|
|
||||||
numpy_unavailable "no bundled Python under ${TEST_DIR}"
|
|
||||||
elif ! has_pinned_numpy; then
|
|
||||||
uv_exe="${python_exe%/python/*}/tools/uv/uv"
|
|
||||||
# Builds that bundle no uv (Windows arm64) fall back to one on PATH.
|
|
||||||
[ -x "${uv_exe}" ] || uv_exe="$(command -v uv)"
|
|
||||||
echo "Installing numpy ${NUMPY_VERSION} into the embedded test interpreter (${python_exe})..."
|
|
||||||
if [ -z "${uv_exe}" ]; then
|
|
||||||
numpy_unavailable "no uv staged beside the tests or on PATH"
|
|
||||||
elif ! "${uv_exe}" pip install --python "${python_exe}" --only-binary :all: "numpy==${NUMPY_VERSION}" \
|
|
||||||
|| ! has_pinned_numpy; then
|
|
||||||
numpy_unavailable "could not install numpy ${NUMPY_VERSION} into ${python_exe}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run the whole suite, excluding tests tagged [NotWorking] and tests labelled RequiresApp,
|
# Run the whole suite, excluding tests tagged [NotWorking] and tests labelled RequiresApp,
|
||||||
# which run the built orca-slicer binary that this directory does not contain.
|
# which run the built orca-slicer binary that this directory does not contain.
|
||||||
# --no-tests=error fails the job if the filter matches nothing (instead of passing green).
|
# --no-tests=error fails the job if the filter matches nothing (instead of passing green).
|
||||||
|
|||||||
@@ -260,6 +260,11 @@ extern bool is_json_file(const std::string& path);
|
|||||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||||
// is rejected on all of them.
|
// is rejected on all of them.
|
||||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||||
|
// True if path names an entry strictly inside root: it must be spelled with root as its prefix,
|
||||||
|
// and must still resolve inside root once symlinks are followed.
|
||||||
|
extern bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root);
|
||||||
|
// True if opening a file with this name through the desktop would run it as a program or script.
|
||||||
|
extern bool is_executable_file_name(const std::string &file_name);
|
||||||
|
|
||||||
// Orca: custom protocal support utils
|
// Orca: custom protocal support utils
|
||||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
#include <boost/shared_ptr.hpp>
|
#include <boost/shared_ptr.hpp>
|
||||||
|
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
#include <boost/algorithm/string/case_conv.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
#include <boost/nowide/fstream.hpp>
|
#include <boost/nowide/fstream.hpp>
|
||||||
@@ -1112,6 +1113,34 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root)
|
||||||
|
{
|
||||||
|
const boost::filesystem::path rel = path.lexically_relative(root);
|
||||||
|
return !rel.empty() && rel != "." && is_path_within_root(rel.string(), root);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_executable_file_name(const std::string &file_name)
|
||||||
|
{
|
||||||
|
static const std::vector<std::string> executable_extensions = {
|
||||||
|
// Windows
|
||||||
|
"exe", "com", "bat", "cmd", "scr", "pif", "msi", "msp", "msc", "cpl", "lnk", "url", "hta", "js", "jse", "vbs",
|
||||||
|
"vbe", "wsf", "wsh", "ps1", "psm1", "reg", "jar", "appref-ms", "scf", "inf", "py", "pyw",
|
||||||
|
// macOS
|
||||||
|
"app", "command", "pkg", "terminal", "workflow",
|
||||||
|
// Linux
|
||||||
|
"sh", "bash", "run", "desktop", "appimage"};
|
||||||
|
// Windows ignores trailing dots and spaces, so "setup.exe." still runs as an .exe.
|
||||||
|
const size_t end = file_name.find_last_not_of(". ");
|
||||||
|
if (end == std::string::npos)
|
||||||
|
return false;
|
||||||
|
const std::string name = file_name.substr(0, end + 1);
|
||||||
|
const size_t dot = name.find_last_of('.');
|
||||||
|
if (dot == std::string::npos || name.find_first_of("/\\", dot) != std::string::npos)
|
||||||
|
return false;
|
||||||
|
const std::string extension = boost::algorithm::to_lower_copy(name.substr(dot + 1));
|
||||||
|
return std::find(executable_extensions.begin(), executable_extensions.end(), extension) != executable_extensions.end();
|
||||||
|
}
|
||||||
|
|
||||||
bool is_img_file(const std::string &path)
|
bool is_img_file(const std::string &path)
|
||||||
{
|
{
|
||||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "GUI_App.hpp"
|
#include "GUI_App.hpp"
|
||||||
#include "GUI_ObjectList.hpp"
|
#include "GUI_ObjectList.hpp"
|
||||||
#include "MainFrame.hpp"
|
#include "MainFrame.hpp"
|
||||||
|
#include "MsgDialog.hpp"
|
||||||
#include <slic3r/GUI/Widgets/WebView.hpp>
|
#include <slic3r/GUI/Widgets/WebView.hpp>
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
@@ -293,9 +294,24 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
|||||||
if (!accessory_path.empty()) {
|
if (!accessory_path.empty()) {
|
||||||
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
||||||
fs::path path(decode_path);
|
fs::path path(decode_path);
|
||||||
|
// Only open the project's own extracted auxiliary files, with the root built as in Reload().
|
||||||
|
fs::path aux_root(encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str()));
|
||||||
|
|
||||||
if (fs::exists(path)) {
|
if (is_absolute_path_within_root(path, aux_root) && fs::is_regular_file(path)) {
|
||||||
wxLaunchDefaultApplication(path.wstring(), 0);
|
// Attachments come with the project, so ask before running one that is a program or script.
|
||||||
|
bool open = true;
|
||||||
|
if (is_executable_file_name(path.filename().string())) {
|
||||||
|
MessageDialog dlg(this,
|
||||||
|
wxString::Format(_L("\"%s\" is a program or script. Opening it will run it on this computer.\n\n"
|
||||||
|
"Only open attachments from projects you trust. Open it anyway?"),
|
||||||
|
from_path(path.filename())),
|
||||||
|
_L("Open attachment"), wxICON_WARNING | wxYES_NO);
|
||||||
|
open = dlg.ShowModal() == wxID_YES;
|
||||||
|
}
|
||||||
|
if (open)
|
||||||
|
wxLaunchDefaultApplication(path.wstring(), 0);
|
||||||
|
} else {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: ignoring path outside the project auxiliary directory: " << decode_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
@@ -116,67 +115,10 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
|||||||
return webui_url;
|
return webui_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Moonraker (Klipper's API server) reports a raised exception as { "error": { "code", "message", "traceback" } }
|
|
||||||
// under every host type that connects to it, often with the cause only in the traceback. Returns the reason to show,
|
|
||||||
// or empty for any other body.
|
|
||||||
std::string moonraker_error_reason(const std::string &body)
|
|
||||||
{
|
|
||||||
const auto root = nlohmann::json::parse(body, nullptr, false);
|
|
||||||
const auto err = root.find("error");
|
|
||||||
if (err == root.end())
|
|
||||||
return {};
|
|
||||||
const auto message = err->find("message");
|
|
||||||
const auto traceback = err->find("traceback");
|
|
||||||
if (message == err->end() || traceback == err->end() || !message->is_string() || !traceback->is_string())
|
|
||||||
return {};
|
|
||||||
|
|
||||||
const auto &msg = message->get_ref<const std::string &>();
|
|
||||||
const auto &tb = traceback->get_ref<const std::string &>();
|
|
||||||
if (msg.empty())
|
|
||||||
return {};
|
|
||||||
const auto end = tb.find_last_not_of(" \t\r\n");
|
|
||||||
if (end == std::string::npos)
|
|
||||||
return msg;
|
|
||||||
|
|
||||||
// Chained exceptions each start a new traceback; the one that failed the request is the last.
|
|
||||||
const auto header = tb.rfind("Traceback (most recent call last):", end);
|
|
||||||
|
|
||||||
// Tornado renders a raised HTTPError as "HTTP <code>: <reason>[ (<detail>)]", and the detail may span lines.
|
|
||||||
const auto code = err->find("code");
|
|
||||||
if (code != err->end() && code->is_number_integer()) {
|
|
||||||
const std::string marker = "HTTP " + std::to_string(code->get<int>()) + ": ";
|
|
||||||
const auto pos = tb.rfind(marker, end);
|
|
||||||
if (pos != std::string::npos && (header == std::string::npos || pos > header) && pos + marker.size() <= end) {
|
|
||||||
const std::string reason = tb.substr(pos + marker.size(), end + 1 - pos - marker.size());
|
|
||||||
// An HTTPError whose detail equals its reason, like HTTPError(401, "Unauthorized"), renders the phrase twice.
|
|
||||||
return reason == msg + " (" + msg + ")" ? msg : reason;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any other exception's type and message are everything from the first unindented line after its frames.
|
|
||||||
auto begin = (header == std::string::npos) ? std::string::npos : tb.find('\n', header);
|
|
||||||
while (begin != std::string::npos && begin < end) {
|
|
||||||
++begin;
|
|
||||||
if (tb[begin] != ' ' && tb[begin] != '\r' && tb[begin] != '\n')
|
|
||||||
break;
|
|
||||||
begin = tb.find('\n', begin);
|
|
||||||
}
|
|
||||||
if (begin == std::string::npos || begin > end) {
|
|
||||||
const auto nl = tb.rfind('\n', end);
|
|
||||||
begin = (nl == std::string::npos) ? 0 : nl + 1;
|
|
||||||
}
|
|
||||||
return msg + " (" + tb.substr(begin, end + 1 - begin) + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
||||||
{
|
{
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
const std::string reason = moonraker_error_reason(body);
|
auto wxbody = wxString::FromUTF8(body.data());
|
||||||
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
|
|
||||||
return wxString::Format("HTTP %u: %s", status, wxbody);
|
return wxString::Format("HTTP %u: %s", status, wxbody);
|
||||||
} else {
|
} else {
|
||||||
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
||||||
|
|||||||
@@ -345,21 +345,16 @@ boost::filesystem::path find_bundled_python_home()
|
|||||||
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
||||||
if (valid_python_home(bundle_python))
|
if (valid_python_home(bundle_python))
|
||||||
return bundle_python;
|
return bundle_python;
|
||||||
#elif !defined(_WIN32)
|
#elif defined(_WIN32)
|
||||||
|
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
||||||
|
if (valid_python_home(exe_python))
|
||||||
|
return exe_python;
|
||||||
|
#else
|
||||||
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
||||||
if (valid_python_home(linux_python))
|
if (valid_python_home(linux_python))
|
||||||
return linux_python;
|
return linux_python;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Next to the executable: the Windows install layout, and the runtime copied
|
|
||||||
// beside every platform's unit-test binary (tests/slic3rutils/CMakeLists.txt).
|
|
||||||
// The CI test runner only receives the build/tests tree, so the candidates
|
|
||||||
// below -- all of which point into the deps or install trees -- never resolve
|
|
||||||
// there.
|
|
||||||
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
|
||||||
if (valid_python_home(exe_python))
|
|
||||||
return exe_python;
|
|
||||||
|
|
||||||
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
||||||
if (!configured_python.empty() && valid_python_home(configured_python))
|
if (!configured_python.empty() && valid_python_home(configured_python))
|
||||||
return configured_python;
|
return configured_python;
|
||||||
|
|||||||
@@ -152,3 +152,65 @@ TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed uncha
|
|||||||
REQUIRE(resolve_cli_input_path("").empty());
|
REQUIRE(resolve_cli_input_path("").empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_absolute_path_within_root accepts only entries inside the root", "[utils]") {
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
ScopedTemporaryDir outer;
|
||||||
|
const fs::path root = outer.path() / "Auxiliaries";
|
||||||
|
fs::create_directories(root / "Others");
|
||||||
|
const fs::path inside = root / "Others" / "note.txt";
|
||||||
|
const fs::path outside = outer.path() / "secret.txt";
|
||||||
|
std::ofstream(inside.string()) << "inside";
|
||||||
|
std::ofstream(outside.string()) << "outside";
|
||||||
|
|
||||||
|
SECTION("a file inside the root") {
|
||||||
|
REQUIRE(is_absolute_path_within_root(inside, root));
|
||||||
|
}
|
||||||
|
SECTION("a path inside the root whose file does not exist yet") {
|
||||||
|
REQUIRE(is_absolute_path_within_root(root / "Others" / "missing.txt", root));
|
||||||
|
}
|
||||||
|
SECTION("the root itself") {
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(root, root));
|
||||||
|
}
|
||||||
|
SECTION("a parent-directory escape spelled under the root") {
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(root / "Others" / ".." / ".." / "secret.txt", root));
|
||||||
|
}
|
||||||
|
SECTION("an absolute path elsewhere") {
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(outside, root));
|
||||||
|
}
|
||||||
|
SECTION("a sibling directory sharing the root's name as a prefix") {
|
||||||
|
const fs::path sibling = outer.path() / "Auxiliaries2" / "note.txt";
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(sibling, root));
|
||||||
|
}
|
||||||
|
SECTION("a relative path") {
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(fs::path("Others") / "note.txt", root));
|
||||||
|
}
|
||||||
|
SECTION("an empty path") {
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(fs::path(), root));
|
||||||
|
}
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Creating symlinks on Windows needs elevated rights or developer mode.
|
||||||
|
SECTION("a symlink inside the root that points outside") {
|
||||||
|
const fs::path link = root / "Others" / "link.txt";
|
||||||
|
fs::create_symlink(outside, link);
|
||||||
|
REQUIRE_FALSE(is_absolute_path_within_root(link, root));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_executable_file_name flags attachments that would run as programs", "[utils]") {
|
||||||
|
const std::string executable = GENERATE(as<std::string>{},
|
||||||
|
"setup.exe", "SETUP.EXE", "Manual.pdf.exe", "run.bat", "start.cmd", "shortcut.lnk", "site.url", "script.ps1",
|
||||||
|
"macro.vbs", "tool.jar", "script.py", "Install.command", "Tool.app", "installer.pkg", "install.sh",
|
||||||
|
"launcher.desktop", "Printer.AppImage", "setup.exe.", "setup.exe ", "setup.exe. .", ".exe");
|
||||||
|
INFO(executable);
|
||||||
|
CHECK(is_executable_file_name(executable));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("is_executable_file_name leaves documents and models alone", "[utils]") {
|
||||||
|
const std::string document = GENERATE(as<std::string>{},
|
||||||
|
"Manual.pdf", "BOM.xlsx", "notes.txt", "photo.JPG", "assembly.step", "part.stl", "project.3mf", "readme",
|
||||||
|
"exe", "setup.exe.pdf", "", "...", "dir.exe/readme");
|
||||||
|
INFO(document);
|
||||||
|
CHECK_FALSE(is_executable_file_name(document));
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_install.cpp
|
test_plugin_install.cpp
|
||||||
test_plugin_lifecycle.cpp
|
test_plugin_lifecycle.cpp
|
||||||
test_plugin_printer_agent.cpp
|
test_plugin_printer_agent.cpp
|
||||||
test_printhost.cpp
|
|
||||||
test_slicing_pipeline_bindings.cpp
|
test_slicing_pipeline_bindings.cpp
|
||||||
test_slicing_pipeline_config.cpp
|
test_slicing_pipeline_config.cpp
|
||||||
test_plugin_sort.cpp
|
test_plugin_sort.cpp
|
||||||
@@ -49,16 +48,9 @@ if (WIN32)
|
|||||||
COMMENT "Copying Python runtime for slic3rutils plugin host API tests"
|
COMMENT "Copying Python runtime for slic3rutils plugin host API tests"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
elseif (NOT FLATPAK)
|
elseif (APPLE)
|
||||||
# The CI unit-test runner only receives the build/tests tree, so both the
|
target_link_options(${_TEST_NAME}_tests PRIVATE
|
||||||
# interpreter and the libpython the test binary links have to travel next to
|
"LINKER:-rpath,@executable_path/python/lib")
|
||||||
# the executable; find_bundled_python_home() picks the copy up from there.
|
|
||||||
if (APPLE)
|
|
||||||
target_link_options(${_TEST_NAME}_tests PRIVATE
|
|
||||||
"LINKER:-rpath,@executable_path/python/lib")
|
|
||||||
else ()
|
|
||||||
set_property(TARGET ${_TEST_NAME}_tests APPEND PROPERTY BUILD_RPATH "$ORIGIN/python/lib")
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
|
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf
|
COMMAND ${CMAKE_COMMAND} -E rm -rf
|
||||||
@@ -66,7 +58,7 @@ elseif (NOT FLATPAK)
|
|||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${CMAKE_PREFIX_PATH}/libpython"
|
"${CMAKE_PREFIX_PATH}/libpython"
|
||||||
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
|
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
|
||||||
COMMENT "Copying Python runtime for the plugin host API tests"
|
COMMENT "Copying Python runtime for macOS plugin host API tests"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
elseif (FLATPAK)
|
elseif (FLATPAK)
|
||||||
@@ -83,16 +75,4 @@ elseif (FLATPAK)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# scripts/run_unit_tests.sh installs the tests' numpy into that runtime with this uv,
|
|
||||||
# staged where the app build tree keeps it (<exe dir>/tools/uv, see src/CMakeLists.txt).
|
|
||||||
if (ORCA_BUNDLED_UV_EXECUTABLE)
|
|
||||||
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/tools/uv"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ORCA_BUNDLED_UV_EXECUTABLE}"
|
|
||||||
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
|
|
||||||
COMMENT "Copying uv for the plugin host API tests"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
#include <catch2/catch_all.hpp>
|
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
#include "slic3r/Utils/PrintHost.hpp"
|
|
||||||
|
|
||||||
using namespace Slic3r;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
class TestPrintHost : public PrintHost
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using PrintHost::format_error;
|
|
||||||
|
|
||||||
const char* get_name() const override { return "Test"; }
|
|
||||||
bool test(wxString&) const override { return true; }
|
|
||||||
wxString get_test_ok_msg() const override { return {}; }
|
|
||||||
wxString get_test_failed_msg(wxString&) const override { return {}; }
|
|
||||||
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; }
|
|
||||||
bool has_auto_discovery() const override { return false; }
|
|
||||||
bool can_test() const override { return false; }
|
|
||||||
PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
|
|
||||||
std::string get_host() const override { return {}; }
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string format_error(const std::string& body, const std::string& error, unsigned status)
|
|
||||||
{
|
|
||||||
return TestPrintHost().format_error(body, error, status).ToStdString();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string envelope(int code, const std::string& message, const std::string& traceback)
|
|
||||||
{
|
|
||||||
return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string moonraker_error(int code, const std::string& message, const std::string& detail = {})
|
|
||||||
{
|
|
||||||
std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message;
|
|
||||||
if (!detail.empty())
|
|
||||||
line += " (" + detail + ")";
|
|
||||||
return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// A real Moonraker body for uploading a file that is being printed.
|
|
||||||
constexpr const char* k_busy_file_403 =
|
|
||||||
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
|
|
||||||
{
|
|
||||||
const std::string msg = format_error(k_busy_file_403, "", 403);
|
|
||||||
INFO("actual: " << msg);
|
|
||||||
|
|
||||||
CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
|
||||||
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback"));
|
|
||||||
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]")
|
|
||||||
{
|
|
||||||
SECTION("a plain detail")
|
|
||||||
{
|
|
||||||
const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted");
|
|
||||||
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a detail whose own parentheses nest (a filename)")
|
|
||||||
{
|
|
||||||
const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)";
|
|
||||||
const std::string body = moonraker_error(400, "Bad Request", detail);
|
|
||||||
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a detail that contains the reason phrase")
|
|
||||||
{
|
|
||||||
const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied");
|
|
||||||
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a detail that spans lines")
|
|
||||||
{
|
|
||||||
const std::string detail = "Move out of range\nX=250.000 Y=10.000";
|
|
||||||
const std::string body = moonraker_error(400, "Bad Request", detail);
|
|
||||||
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a detail that only repeats the reason phrase is dropped")
|
|
||||||
{
|
|
||||||
const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized");
|
|
||||||
CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]")
|
|
||||||
{
|
|
||||||
const std::string frame = "Traceback (most recent call last):\n"
|
|
||||||
" File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n"
|
|
||||||
" self._write(data)\n";
|
|
||||||
|
|
||||||
SECTION("a one-line message")
|
|
||||||
{
|
|
||||||
const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n");
|
|
||||||
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a message that spans lines")
|
|
||||||
{
|
|
||||||
const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n");
|
|
||||||
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("raised while handling an HTTPError with the same code")
|
|
||||||
{
|
|
||||||
const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n"
|
|
||||||
"During handling of the above exception, another exception occurred:\n\n" +
|
|
||||||
frame + "OSError: [Errno 5] Input/output error\n";
|
|
||||||
const std::string body = envelope(500, "Internal Server Error", traceback);
|
|
||||||
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a traceback with no header")
|
|
||||||
{
|
|
||||||
const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error");
|
|
||||||
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]")
|
|
||||||
{
|
|
||||||
SECTION("message is the whole reason, no trailing detail")
|
|
||||||
{
|
|
||||||
const std::string body = moonraker_error(503, "Klippy is not ready");
|
|
||||||
CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a message that itself contains parentheses is not duplicated")
|
|
||||||
{
|
|
||||||
const std::string reason = "Requested blocks (0-5) are unavailable";
|
|
||||||
const std::string body = moonraker_error(400, reason);
|
|
||||||
CHECK(format_error(body, "", 400) == "HTTP 400: " + reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]")
|
|
||||||
{
|
|
||||||
struct Case
|
|
||||||
{
|
|
||||||
const char* name;
|
|
||||||
const char* body;
|
|
||||||
unsigned status;
|
|
||||||
const char* expected;
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto c = GENERATE(
|
|
||||||
Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500,
|
|
||||||
"HTTP 500: Internal Server Error"},
|
|
||||||
Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON",
|
|
||||||
500, "HTTP 500: Internal Server Error"});
|
|
||||||
|
|
||||||
DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); }
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]")
|
|
||||||
{
|
|
||||||
const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full");
|
|
||||||
CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]")
|
|
||||||
{
|
|
||||||
SECTION("OctoPrint's string-valued error member")
|
|
||||||
{
|
|
||||||
const std::string body = R"JSON({"error": "File not found"})JSON";
|
|
||||||
CHECK(format_error(body, "", 404) == "HTTP 404: " + body);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("PrusaLink's top-level message, not under error")
|
|
||||||
{
|
|
||||||
const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON";
|
|
||||||
CHECK(format_error(body, "", 409) == "HTTP 409: " + body);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a body that is not JSON")
|
|
||||||
{
|
|
||||||
const std::string html = "<html><head><title>502 Bad Gateway</title></head></html>";
|
|
||||||
CHECK(format_error(html, "", 502) == "HTTP 502: " + html);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("an error object with no traceback")
|
|
||||||
{
|
|
||||||
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON";
|
|
||||||
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("an error object whose traceback is null")
|
|
||||||
{
|
|
||||||
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON";
|
|
||||||
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("an envelope whose reason phrase is empty")
|
|
||||||
{
|
|
||||||
const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n");
|
|
||||||
CHECK(format_error(body, "", 403) == "HTTP 403: " + body);
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("a transport error with no HTTP status")
|
|
||||||
{
|
|
||||||
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user