import base64 import hashlib import json import os import shutil import stat import tempfile import FreeCAD as App FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_FILEINCLUDED_SUCCESS_OUTPUT", "") HOSTS = [ ("App::DocumentObjectFileIncluded", "File"), ("App::VRMLObject", "VrmlFile"), ("Image::ImagePlane", "ImageFile"), ("Robot::RobotObject", "RobotKinematicFile"), ("Robot::RobotObject", "RobotVrmlFile"), ("Sketcher::SketchObjectSF", "SketchFlatFile"), ("TechDraw::DrawComplexSection", "PatIncluded"), ("TechDraw::DrawComplexSection", "SvgIncluded"), ("TechDraw::DrawComplexSectionPython", "PatIncluded"), ("TechDraw::DrawComplexSectionPython", "SvgIncluded"), ("TechDraw::DrawGeomHatch", "PatIncluded"), ("TechDraw::DrawHatch", "SvgIncluded"), ("TechDraw::DrawSVGTemplate", "PageResult"), ("TechDraw::DrawTileWeld", "SymbolIncluded"), ("TechDraw::DrawTileWeldPython", "SymbolIncluded"), ("TechDraw::DrawViewImage", "ImageIncluded"), ("TechDraw::DrawViewSection", "PatIncluded"), ("TechDraw::DrawViewSection", "SvgIncluded"), ("TechDraw::DrawViewSectionPython", "PatIncluded"), ("TechDraw::DrawViewSectionPython", "SvgIncluded"), ] def sha256_file(path): with open(path, "rb") as handle: return hashlib.sha256(handle.read()).hexdigest() def write_asset(directory, name, data): path = os.path.join(directory, name) with open(path, "wb") as handle: handle.write(data) return path def create_assets(directory): png = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" ) kinematic = ( "a,alpha,d,theta,rotDir,maxAngle,minAngle,velocity\n" "500,-90,1045,0,-1,185,-185,156\n" "1300,0,0,0,1,35,-155,156\n" "55,90,0,-90,1,154,-130,156\n" "0,-90,-1025,0,1,350,-350,330\n" "0,90,0,0,1,130,-130,330\n" "0,180,-300,0,1,350,-350,615\n" ).encode("ascii") return { "bin": write_asset(directory, "payload.bin", b"FreeCAD-PropertyFileIncluded\x00success\xff\n"), "bin2": write_asset(directory, "payload-two.bin", b"FreeCAD-PropertyFileIncluded\x00second\x80\n"), "svg": write_asset(directory, "pattern.svg", b'\n'), "pat": write_asset(directory, "pattern.pat", b"*TEST,PropertyFileIncluded\n0, 0,0, 0,2\n"), "png": write_asset(directory, "pixel.png", png), "wrl": write_asset(directory, "model.wrl", b"#VRML V2.0 utf8\nShape { geometry Box { size 1 1 1 } }\n"), "csv": write_asset(directory, "kinematic.csv", kinematic), "skf": write_asset(directory, "sketch.skf", b"# SketchFlat PropertyFileIncluded fixture\n"), } def asset_for(object_type_id, property_name, assets): if property_name in ("PatIncluded",): return assets["pat"], "pat" if property_name in ("SvgIncluded", "PageResult", "SymbolIncluded"): return assets["svg"], "svg" if property_name in ("ImageFile", "ImageIncluded"): return assets["png"], "png" if property_name in ("VrmlFile", "RobotVrmlFile"): return assets["wrl"], "wrl" if property_name == "RobotKinematicFile": return assets["csv"], "csv" if property_name == "SketchFlatFile": return assets["skf"], "skf" return assets["bin"], "bin" def shape_snapshot(obj): if "Shape" not in obj.PropertiesList: return {"applicable": False, "reason": "host-has-no-shape-property"} shape = obj.Shape if shape is None or shape.isNull(): return {"applicable": True, "isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0} return { "applicable": True, "isNull": False, "isValid": bool(shape.isValid()), "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), } def file_snapshot(document, obj, property_name): path = str(getattr(obj, property_name)) exists = os.path.isfile(path) if path else False mode = stat.S_IMODE(os.stat(path).st_mode) if exists else None return { "value": path, "baseName": os.path.basename(path) if path else "", "exists": exists, "bytes": os.path.getsize(path) if exists else 0, "sha256": sha256_file(path) if exists else None, "mode": mode, "writeBits": mode & 0o222 if mode is not None else None, "underTransientDir": bool(path) and os.path.commonpath([os.path.abspath(path), os.path.abspath(document.TransientDir)]) == os.path.abspath(document.TransientDir), "propertyTypeId": obj.getTypeIdOfProperty(property_name), "propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)], "editorMode": [str(item) for item in obj.getEditorMode(property_name)], "objectState": [str(item) for item in obj.State], "statusString": str(obj.getStatusString()), "shape": shape_snapshot(obj), "objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], } def run_setter_variants(assets): document = App.newDocument("PropertyFileIncludedSetterSuccess") try: phases = [] def assign(name, value, expected_asset, expected_base_name=None): obj = document.addObject("App::DocumentObjectFileIncluded", name) setattr(obj, "File", value) snapshot = file_snapshot(document, obj, "File") snapshot.update({ "id": name, "sourcePath": expected_asset, "sourceSha256": sha256_file(expected_asset), "expectedBaseName": expected_base_name or os.path.basename(expected_asset), }) phases.append(snapshot) return obj assign("StringPath", assets["bin"], assets["bin"]) assign("BytesPath", os.fsencode(assets["bin2"]), assets["bin2"]) assign("TupleRename", (assets["bin"], "renamed.dat"), assets["bin"], "renamed.dat") with open(assets["bin2"], "rb") as source: assign("OpenIoFile", source, assets["bin2"]) assign("Dictionary", {"filename": assets["bin"], "filter": "Binary files (*.bin)"}, assets["bin"]) empty_no_op = document.addObject("App::DocumentObjectFileIncluded", "EmptyNoOp") empty_no_op.File = assets["bin"] before_empty = file_snapshot(document, empty_no_op, "File") empty_no_op.File = "" after_empty = file_snapshot(document, empty_no_op, "File") collision_a = assign("CollisionA", (assets["bin"], "collision.bin"), assets["bin"], "collision.bin") collision_b = assign("CollisionB", (assets["bin2"], "collision.bin"), assets["bin2"], "collision1.bin") recompute_result = bool(document.recompute()) return { "phases": phases, "emptyString": {"before": before_empty, "after": after_empty, "preserved": before_empty["value"] == after_empty["value"] and before_empty["sha256"] == after_empty["sha256"]}, "collision": { "first": file_snapshot(document, collision_a, "File"), "second": file_snapshot(document, collision_b, "File"), "distinctPaths": str(collision_a.File) != str(collision_b.File), "distinctBytes": sha256_file(str(collision_a.File)) != sha256_file(str(collision_b.File)), }, "recomputeResult": recompute_result, "objectCount": len(document.Objects), } finally: App.closeDocument(document.Name) def run_host_case(index, object_type_id, property_name, assets): document = App.newDocument("PropertyFileIncludedHost%02d" % index) try: obj = document.addObject(object_type_id, "HostProbe") before_object_set = [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects] default = file_snapshot(document, obj, property_name) source_path, asset_kind = asset_for(object_type_id, property_name, assets) archive_name = "host-%02d.%s" % (index, asset_kind) setattr(obj, property_name, (source_path, archive_name)) after_set = file_snapshot(document, obj, property_name) recompute_result = bool(document.recompute()) after_recompute = file_snapshot(document, obj, property_name) return { "objectTypeId": object_type_id, "propertyName": property_name, "group": obj.getGroupOfProperty(property_name), "source": {"path": source_path, "kind": asset_kind, "bytes": os.path.getsize(source_path), "sha256": sha256_file(source_path)}, "archiveName": archive_name, "default": default, "afterSet": after_set, "afterRecompute": after_recompute, "recomputeResult": recompute_result, "objectSetStable": before_object_set == after_set["objectSet"] == after_recompute["objectSet"], } finally: App.closeDocument(document.Name) def run(): asset_directory = tempfile.mkdtemp(prefix="freecad-property-fileincluded-success-") try: assets = create_assets(asset_directory) setter_variants = run_setter_variants(assets) host_cases = [run_host_case(index, *host, assets) for index, host in enumerate(HOSTS)] return { "schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-fileincluded-success", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyFileIncluded", "setterVariants": setter_variants, "hostCaseCount": len(host_cases), "hostCases": host_cases, "diagnostics": {"exception": None}, } finally: shutil.rmtree(asset_directory, ignore_errors=True) report = run() if not OUTPUT_PATH: raise RuntimeError("FREECAD_PROPERTY_FILEINCLUDED_SUCCESS_OUTPUT is required") with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: json.dump(report, handle, indent=2, sort_keys=True) handle.write("\n") print("FREECAD_PROPERTY_FILEINCLUDED_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "hostCaseCount": report["hostCaseCount"], "setterPhaseCount": len(report["setterVariants"]["phases"])}, sort_keys=True))