Merge branch 'devel' into test/immediateM112
This commit is contained in:
commit
a7f5e07271
28 changed files with 4684 additions and 84 deletions
3
setup.py
3
setup.py
|
|
@ -31,7 +31,8 @@ INSTALL_REQUIRES = [
|
|||
"rsa",
|
||||
"pkginfo",
|
||||
"requests",
|
||||
"semantic_version"
|
||||
"semantic_version",
|
||||
"psutil"
|
||||
]
|
||||
|
||||
# Additional requirements for optional install options
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import threading
|
|||
import time
|
||||
import logging
|
||||
import logging.handlers
|
||||
import hashlib
|
||||
|
||||
from . import version_checks, updaters, exceptions, util
|
||||
|
||||
|
|
@ -99,8 +100,12 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
self._logger.exception("Error while loading version cache from disk")
|
||||
else:
|
||||
try:
|
||||
if "octoprint" in data and len(data["octoprint"]) == 4 and "local" in data["octoprint"][1] and "value" in data["octoprint"][1]["local"]:
|
||||
data_version = data["octoprint"][1]["local"]["value"]
|
||||
if not isinstance(data, dict):
|
||||
self._logger.info("Version cache was created in a different format, not using it")
|
||||
return
|
||||
|
||||
if "__version" in data:
|
||||
data_version = data["__version"]
|
||||
else:
|
||||
self._logger.info("Can't determine version of OctoPrint version cache was created for, not using it")
|
||||
return
|
||||
|
|
@ -118,24 +123,18 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
self._logger.exception("Error parsing in version cache data")
|
||||
|
||||
def _save_version_cache(self):
|
||||
import tempfile
|
||||
import yaml
|
||||
import shutil
|
||||
from octoprint.util import atomic_write
|
||||
from octoprint._version import get_versions
|
||||
|
||||
file_obj = tempfile.NamedTemporaryFile(delete=False)
|
||||
try:
|
||||
octoprint_version = get_versions()["version"]
|
||||
self._version_cache["__version"] = octoprint_version
|
||||
|
||||
with atomic_write(self._version_cache_path) as file_obj:
|
||||
yaml.safe_dump(self._version_cache, stream=file_obj, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
file_obj.close()
|
||||
shutil.move(file_obj.name, self._version_cache_path)
|
||||
|
||||
self._version_cache_dirty = False
|
||||
self._logger.info("Saved version cache to disk")
|
||||
finally:
|
||||
try:
|
||||
if os.path.exists(file_obj.name):
|
||||
os.remove(file_obj.name)
|
||||
except Exception as e:
|
||||
self._logger.warn("Could not delete file {}: {}".format(file_obj.name, str(e)))
|
||||
self._version_cache_dirty = False
|
||||
self._logger.info("Saved version cache to disk")
|
||||
|
||||
#~~ SettingsPlugin API
|
||||
|
||||
|
|
@ -159,20 +158,62 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
data = dict(octoprint.plugin.SettingsPlugin.on_settings_load(self))
|
||||
if "checks" in data:
|
||||
del data["checks"]
|
||||
|
||||
checks = self._get_configured_checks()
|
||||
if "octoprint" in checks:
|
||||
if "checkout_folder" in checks["octoprint"]:
|
||||
data["octoprint_checkout_folder"] = checks["octoprint"]["checkout_folder"]
|
||||
elif "update_folder" in checks["octoprint"]:
|
||||
data["octoprint_checkout_folder"] = checks["octoprint"]["update_folder"]
|
||||
else:
|
||||
data["octoprint_checkout_folder"] = None
|
||||
data["octoprint_type"] = checks["octoprint"].get("type", None)
|
||||
else:
|
||||
data["octoprint_checkout_folder"] = None
|
||||
data["octoprint_type"] = None
|
||||
|
||||
return data
|
||||
|
||||
def on_settings_save(self, data):
|
||||
for key in self.get_settings_defaults():
|
||||
if key == "checks" or key == "cache_ttl":
|
||||
if key == "checks" or key == "cache_ttl" or key == "octoprint_checkout_folder" or key == "octoprint_type":
|
||||
continue
|
||||
if key in data:
|
||||
self._settings.set([key], data[key])
|
||||
|
||||
if "cache_ttl" in data:
|
||||
self._settings.set_int(["cache_ttl"], data["cache_ttl"])
|
||||
|
||||
self._version_cache_ttl = self._settings.get_int(["cache_ttl"]) * 60
|
||||
|
||||
checks = self._get_configured_checks()
|
||||
if "octoprint" in checks:
|
||||
check = checks["octoprint"]
|
||||
update_type = check.get("type", None)
|
||||
checkout_folder = check.get("checkout_folder", None)
|
||||
update_folder = check.get("update_folder", None)
|
||||
|
||||
defaults = dict(
|
||||
plugins=dict(softwareupdate=dict(
|
||||
checks=dict(
|
||||
octoprint=dict(
|
||||
type=update_type,
|
||||
checkout_folder=checkout_folder,
|
||||
update_folder=update_folder
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
if "octoprint_checkout_folder" in data:
|
||||
self._settings.set(["checks", "octoprint", "checkout_folder"], data["octoprint_checkout_folder"], defaults=defaults, force=True)
|
||||
if update_folder and data["octoprint_checkout_folder"]:
|
||||
self._settings.set(["checks", "octoprint", "update_folder"], None, defaults=defaults, force=True)
|
||||
self._refresh_configured_checks = True
|
||||
|
||||
if "octoprint_type" in data and data["octoprint_type"] in ("github_release", "git_commit"):
|
||||
self._settings.set(["checks", "octoprint", "type"], data["octoprint_type"], defaults=defaults, force=True)
|
||||
self._refresh_configured_checks = True
|
||||
|
||||
def get_settings_version(self):
|
||||
return 4
|
||||
|
||||
|
|
@ -380,7 +421,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
try:
|
||||
target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force)
|
||||
if target_information is None:
|
||||
continue
|
||||
target_information = dict()
|
||||
except exceptions.UnknownCheckType:
|
||||
self._logger.warn("Unknown update check type for %s" % target)
|
||||
continue
|
||||
|
|
@ -399,22 +440,29 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
updatePossible=target_update_possible,
|
||||
information=target_information,
|
||||
displayName=populated_check["displayName"],
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value))
|
||||
displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value),
|
||||
check=populated_check)
|
||||
|
||||
if self._version_cache_dirty:
|
||||
self._save_version_cache()
|
||||
return information, update_available, update_possible
|
||||
|
||||
def _get_check_hash(self, check):
|
||||
hash = hashlib.md5()
|
||||
hash.update(repr(check))
|
||||
return hash.hexdigest()
|
||||
|
||||
def _get_current_version(self, target, check, force=False):
|
||||
"""
|
||||
Determines the current version information for one target based on its check configuration.
|
||||
"""
|
||||
|
||||
current_hash = self._get_check_hash(check)
|
||||
if target in self._version_cache and not force:
|
||||
timestamp, information, update_available, update_possible = self._version_cache[target]
|
||||
if timestamp + self._version_cache_ttl >= time.time() > timestamp:
|
||||
data = self._version_cache[target]
|
||||
if data["hash"] == current_hash and data["timestamp"] + self._version_cache_ttl >= time.time() > data["timestamp"]:
|
||||
# we also check that timestamp < now to not get confused too much by clock changes
|
||||
return information, update_available, update_possible
|
||||
return data["information"], data["available"], data["possible"]
|
||||
|
||||
information = dict()
|
||||
update_available = False
|
||||
|
|
@ -437,7 +485,11 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
|
|||
except:
|
||||
update_possible = False
|
||||
|
||||
self._version_cache[target] = (time.time(), information, update_available, update_possible)
|
||||
self._version_cache[target] = dict(timestamp=time.time(),
|
||||
hash=current_hash,
|
||||
information=information,
|
||||
available=update_available,
|
||||
possible=update_possible)
|
||||
self._version_cache_dirty = True
|
||||
return information, update_available, update_possible
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,20 @@ $(function() {
|
|||
self.workingOutput = undefined;
|
||||
self.loglines = ko.observableArray([]);
|
||||
|
||||
self.octoprintUnconfigured = ko.observable();
|
||||
self.octoprintUnreleased = ko.observable();
|
||||
|
||||
self.config_cacheTtl = ko.observable();
|
||||
self.config_checkoutFolder = ko.observable();
|
||||
self.config_checkType = ko.observable();
|
||||
|
||||
self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog");
|
||||
|
||||
self.config_availableCheckTypes = [
|
||||
{"key": "github_release", "name": gettext("Release")},
|
||||
{"key": "git_commit", "name": gettext("Commit")}
|
||||
];
|
||||
|
||||
self.versions = new ItemListHelper(
|
||||
"plugin.softwareupdate.versions",
|
||||
{
|
||||
|
|
@ -82,15 +92,23 @@ $(function() {
|
|||
var data = {
|
||||
plugins: {
|
||||
softwareupdate: {
|
||||
cache_ttl: parseInt(self.config_cacheTtl())
|
||||
cache_ttl: parseInt(self.config_cacheTtl()),
|
||||
octoprint_checkout_folder: self.config_checkoutFolder(),
|
||||
octoprint_type: self.config_checkType()
|
||||
}
|
||||
}
|
||||
};
|
||||
self.settings.saveData(data, function() { self.configurationDialog.modal("hide"); self._copyConfig(); });
|
||||
self.settings.saveData(data, function() {
|
||||
self.configurationDialog.modal("hide");
|
||||
self._copyConfig();
|
||||
self.performCheck();
|
||||
});
|
||||
};
|
||||
|
||||
self._copyConfig = function() {
|
||||
self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl());
|
||||
self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder());
|
||||
self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type());
|
||||
};
|
||||
|
||||
self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) {
|
||||
|
|
@ -109,6 +127,25 @@ $(function() {
|
|||
});
|
||||
self.versions.updateItems(versions);
|
||||
|
||||
var octoprint = data.information["octoprint"];
|
||||
if (octoprint && octoprint.hasOwnProperty("check")) {
|
||||
var check = octoprint.check;
|
||||
if (BRANCH != "master" && check["type"] == "github_release") {
|
||||
self.octoprintUnreleased(true);
|
||||
} else {
|
||||
self.octoprintUnreleased(false);
|
||||
}
|
||||
|
||||
var checkoutFolder = (check["checkout_folder"] || "").trim();
|
||||
var updateFolder = (check["update_folder"] || "").trim();
|
||||
var checkType = check["type"] || "";
|
||||
if ((checkType == "github_release" || checkType == "git_commit") && checkoutFolder == "" && updateFolder == "") {
|
||||
self.octoprintUnconfigured(true);
|
||||
} else {
|
||||
self.octoprintUnconfigured(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.status == "updateAvailable" || data.status == "updatePossible") {
|
||||
var text = gettext("There are updates available for the following components:");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
<div class="alert" data-bind="visible: octoprintUnconfigured()">{% trans %}
|
||||
Please configure the <strong>checkout folder</strong> of OctoPrint, otherwise
|
||||
this plugin won't be able to update it. Click on the <i class="icon-wrench"></i> button
|
||||
to do this. Also refer to the <a href="https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update" target="_blank">Documentation</a>.
|
||||
{% endtrans %}</div>
|
||||
<div class="alert" data-bind="visible: !octoprintUnconfigured() && octoprintUnreleased()">{% trans %}
|
||||
<p>
|
||||
<strong>You are running a non-release version of OctoPrint but are tracking OctoPrint
|
||||
releases.</strong>
|
||||
</p><p>
|
||||
You probably want OctoPrint to track the matching development version instead.
|
||||
If you have a local OctoPrint checkout folder switched to another branch,
|
||||
<strong>simply switching over to "Commit" tracking</strong> will already
|
||||
take care of that. Otherwise please take a look at the
|
||||
<a href="https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update" target="_blank">Documentation</a>.
|
||||
</p>
|
||||
{% endtrans %}</div>
|
||||
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-small" data-bind="click: function() { $root.showPluginSettings(); }" title="{{ _('Plugin Configuration') }}"><i class="icon-wrench"></i></button>
|
||||
</div>
|
||||
|
|
@ -11,7 +29,7 @@
|
|||
<span data-bind="invisible: !updateAvailable"><i class="icon-bell" title="{{ _('Update available') }}"></i></span>
|
||||
</td>
|
||||
<td class="settings_plugin_softwareupdate_column_information">
|
||||
<strong data-bind="text: displayName"></strong>: <span data-bind="text: displayVersion"></span><br>
|
||||
<strong data-bind="text: displayName"></strong>: <span data-bind="text: displayVersion"></span> <span data-bind="invisible: updatePossible"><i class="icon-exclamation-sign" title="{{ _('Update not possible, configuration ok?') }}"></i></span><br>
|
||||
<small class="muted">
|
||||
{{ _('Installed:') }} <span data-bind="text: information.local.name"></span><br>
|
||||
{{ _('Available:') }} <span data-bind="text: information.remote.name"></span>
|
||||
|
|
@ -44,7 +62,7 @@
|
|||
<div><small><a href="#" class="muted" onclick="$(this).children().toggleClass('icon-caret-right icon-caret-down').parent().parent().parent().next().slideToggle('fast')"><i class="icon-caret-right"></i> {{ _('Advanced options') }}</a></small></div>
|
||||
<div class="hide">
|
||||
<button class="btn btn-block" data-bind="click: function() { $root.performCheck(true, true, true); }">{{ _('Force check for update (overrides cache used for update checks)') }}</button>
|
||||
<button class="btn btn-block btn-danger" data-bind="click: function() { $root.update(true); }">{{ _('Force update now (even if no new versions are available)') }}</button>
|
||||
<button class="btn btn-block btn-danger" data-bind="visible: CONFIG_DEBUG, click: function() { $root.update(true); }">{{ _('Force update now (even if no new versions are available)') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -55,6 +73,18 @@
|
|||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('OctoPrint checkout folder') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: config_checkoutFolder">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('OctoPrint version tracking') }}</label>
|
||||
<div class="controls">
|
||||
<select data-bind="value: config_checkType, options: config_availableCheckTypes, optionsText: 'name', optionsValue: 'key'"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{ _('Version cache TTL') }}</label>
|
||||
<div class="controls">
|
||||
|
|
|
|||
|
|
@ -37,7 +37,17 @@ def _get_caller(log_cb=None):
|
|||
|
||||
|
||||
def can_perform_update(target, check):
|
||||
return "update_script" in check and ("checkout_folder" in check or "update_folder" in check)
|
||||
import os
|
||||
script_configured = bool("update_script" in check and check["update_script"])
|
||||
|
||||
folder = None
|
||||
if "update_folder" in check:
|
||||
folder = check["update_folder"]
|
||||
elif "checkout_folder" in check:
|
||||
folder = check["checkout_folder"]
|
||||
folder_configured = bool(folder and os.path.isdir(folder))
|
||||
|
||||
return script_configured and folder_configured
|
||||
|
||||
|
||||
def perform_update(target, check, target_version, log_cb=None):
|
||||
|
|
|
|||
|
|
@ -314,8 +314,13 @@ class VirtualPrinter(object):
|
|||
else:
|
||||
self._send("Error: expected line %d got %d" % (expected, actual))
|
||||
|
||||
self._send("Resend:%d" % expected)
|
||||
self._send("ok")
|
||||
def request_resend():
|
||||
self._send("Resend:%d" % expected)
|
||||
self._send("ok")
|
||||
|
||||
if settings().getBoolean(["devel", "virtualPrinter", "repetierStyleResends"]):
|
||||
request_resend()
|
||||
request_resend()
|
||||
|
||||
def _debugTrigger(self, data):
|
||||
if data == "action_pause":
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
|
||||
from flask import request, jsonify, make_response, url_for
|
||||
|
||||
import octoprint.util as util
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
from octoprint.server import printer, fileManager, slicingManager, eventManager, NO_CONTENT
|
||||
|
|
@ -18,6 +17,8 @@ import octoprint.filemanager
|
|||
import octoprint.filemanager.util
|
||||
import octoprint.slicing
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
#~~ GCODE file handling
|
||||
|
||||
|
|
@ -29,7 +30,8 @@ def readGcodeFiles():
|
|||
filter = request.values["filter"]
|
||||
files = _getFileList(FileDestinations.LOCAL, filter=filter)
|
||||
files.extend(_getFileList(FileDestinations.SDCARD))
|
||||
return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads")))
|
||||
usage = psutil.disk_usage(settings().getBaseFolder("uploads"))
|
||||
return jsonify(files=files, free=usage.free, total=usage.total)
|
||||
|
||||
|
||||
@api.route("/files/<string:origin>", methods=["GET"])
|
||||
|
|
@ -40,7 +42,8 @@ def readGcodeFilesForOrigin(origin):
|
|||
files = _getFileList(origin)
|
||||
|
||||
if origin == FileDestinations.LOCAL:
|
||||
return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads")))
|
||||
usage = psutil.disk_usage(settings().getBaseFolder("uploads"))
|
||||
return jsonify(files=files, free=usage.free, total=usage.total)
|
||||
else:
|
||||
return jsonify(files=files)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,15 +15,18 @@ from octoprint.settings import settings
|
|||
from octoprint.server import NO_CONTENT, admin_permission
|
||||
from octoprint.server.util.flask import redirect_to_tornado, restricted_access
|
||||
from octoprint.server.api import api
|
||||
from octoprint.util import get_free_bytes
|
||||
|
||||
|
||||
@api.route("/logs", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getLogFiles():
|
||||
import psutil
|
||||
usage = psutil.disk_usage(settings().getBaseFolder("logs"))
|
||||
|
||||
files = _getLogFiles()
|
||||
return jsonify(files=files, free=get_free_bytes(settings().getBaseFolder("logs")))
|
||||
|
||||
return jsonify(files=files, free=usage.free, total=usage.total)
|
||||
|
||||
|
||||
@api.route("/logs/<path:filename>", methods=["GET"])
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ def getSettings():
|
|||
"repetierTargetTemp": s.getBoolean(["feature", "repetierTargetTemp"]),
|
||||
"externalHeatupDetection": s.getBoolean(["feature", "externalHeatupDetection"]),
|
||||
"keyboardControl": s.getBoolean(["feature", "keyboardControl"]),
|
||||
"pollWatched": s.getBoolean(["feature", "pollWatched"])
|
||||
"pollWatched": s.getBoolean(["feature", "pollWatched"]),
|
||||
"ignoreIdenticalResends": s.getBoolean(["feature", "ignoreIdenticalResends"])
|
||||
},
|
||||
"serial": {
|
||||
"port": connectionOptions["portPreference"],
|
||||
|
|
@ -122,6 +123,10 @@ def getSettings():
|
|||
"systemShutdownCommand": s.get(["server", "commands", "systemShutdownCommand"]),
|
||||
"systemRestartCommand": s.get(["server", "commands", "systemRestartCommand"]),
|
||||
"serverRestartCommand": s.get(["server", "commands", "serverRestartCommand"])
|
||||
},
|
||||
"diskspace": {
|
||||
"warning": s.getInt(["server", "diskspace", "warning"]),
|
||||
"critical": s.getInt(["server", "diskspace", "critical"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -217,6 +222,7 @@ def _saveSettings(data):
|
|||
if "externalHeatupDetection" in data["feature"].keys(): s.setBoolean(["feature", "externalHeatupDetection"], data["feature"]["externalHeatupDetection"])
|
||||
if "keyboardControl" in data["feature"].keys(): s.setBoolean(["feature", "keyboardControl"], data["feature"]["keyboardControl"])
|
||||
if "pollWatched" in data["feature"]: s.setBoolean(["feature", "pollWatched"], data["feature"]["pollWatched"])
|
||||
if "ignoreIdenticalResends" in data["feature"]: s.setBoolean(["feature", "ignoreIdenticalResends"], data["feature"]["ignoreIdenticalResends"])
|
||||
|
||||
if "serial" in data.keys():
|
||||
if "autoconnect" in data["serial"].keys(): s.setBoolean(["serial", "autoconnect"], data["serial"]["autoconnect"])
|
||||
|
|
@ -274,6 +280,9 @@ def _saveSettings(data):
|
|||
if "systemShutdownCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "systemShutdownCommand"], data["server"]["commands"]["systemShutdownCommand"])
|
||||
if "systemRestartCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "systemRestartCommand"], data["server"]["commands"]["systemRestartCommand"])
|
||||
if "serverRestartCommand" in data["server"]["commands"].keys(): s.set(["server", "commands", "serverRestartCommand"], data["server"]["commands"]["serverRestartCommand"])
|
||||
if "diskspace" in data["server"]:
|
||||
if "warning" in data["server"]["diskspace"]: s.setInt(["server", "diskspace", "warning"], data["server"]["diskspace"]["warning"])
|
||||
if "critical" in data["server"]["diskspace"]: s.setInt(["server", "diskspace", "critical"], data["server"]["diskspace"]["critical"])
|
||||
|
||||
if "plugins" in data:
|
||||
for plugin in octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin):
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
|
|||
apikey=octoprint.server.UI_API_KEY,
|
||||
version=octoprint.server.VERSION,
|
||||
display_version=octoprint.server.DISPLAY_VERSION,
|
||||
branch=octoprint.server.BRANCH,
|
||||
plugin_hash=plugin_hash.hexdigest(),
|
||||
config_hash=config_hash
|
||||
))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from flask import request, g, url_for, make_response, render_template, send_from
|
|||
import octoprint.plugin
|
||||
|
||||
from octoprint.server import app, userManager, pluginManager, gettext, \
|
||||
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY
|
||||
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY, BRANCH
|
||||
from octoprint.settings import settings
|
||||
|
||||
from . import util
|
||||
|
|
@ -311,6 +311,7 @@ def index():
|
|||
debug=debug,
|
||||
version=VERSION,
|
||||
display_version=DISPLAY_VERSION,
|
||||
branch=BRANCH,
|
||||
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
|
||||
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
|
||||
uiApiKey=UI_API_KEY,
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ default_settings = {
|
|||
"additionalBaudrates": [],
|
||||
"longRunningCommands": ["G4", "G28", "G29", "G30", "G32", "M400", "M226"],
|
||||
"checksumRequiringCommands": ["M110"],
|
||||
"helloCommand": "M110 N0"
|
||||
"helloCommand": "M110 N0",
|
||||
},
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
|
|
@ -112,6 +112,10 @@ default_settings = {
|
|||
"systemShutdownCommand": None,
|
||||
"systemRestartCommand": None,
|
||||
"serverRestartCommand": None
|
||||
},
|
||||
"diskspace": {
|
||||
"warning": 500 * 1024 * 1024, # 500 MB
|
||||
"critical": 200 * 1024 * 1024, # 200 MB
|
||||
}
|
||||
},
|
||||
"webcam": {
|
||||
|
|
@ -152,7 +156,9 @@ default_settings = {
|
|||
"externalHeatupDetection": True,
|
||||
"supportWait": True,
|
||||
"keyboardControl": True,
|
||||
"pollWatched": False
|
||||
"pollWatched": False,
|
||||
"ignoreIdenticalResends": False,
|
||||
"identicalResendsCountdown": 7
|
||||
},
|
||||
"folder": {
|
||||
"uploads": None,
|
||||
|
|
@ -281,6 +287,7 @@ default_settings = {
|
|||
},
|
||||
"hasBed": True,
|
||||
"repetierStyleTargetTemperature": False,
|
||||
"repetierStyleResends": False,
|
||||
"okBeforeCommandOutput": False,
|
||||
"smoothieTemperatureReporting": False,
|
||||
"extendedSdFileList": False,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ function DataUpdater(allViewModels) {
|
|||
var oldVersion = VERSION;
|
||||
VERSION = data["version"];
|
||||
DISPLAY_VERSION = data["display_version"];
|
||||
BRANCH = data["branch"];
|
||||
$("span.version").text(DISPLAY_VERSION);
|
||||
|
||||
var oldPluginHash = self._pluginHash;
|
||||
|
|
|
|||
|
|
@ -333,6 +333,34 @@ function formatSize(bytes) {
|
|||
return _.sprintf("%.1f%s", bytes, "TB");
|
||||
}
|
||||
|
||||
function bytesFromSize(size) {
|
||||
if (size == undefined || size.trim() == "") return undefined;
|
||||
|
||||
var parsed = size.match(/^([+]?[0-9]*\.?[0-9]+)(?:\s*)?(.*)$/);
|
||||
var number = parsed[1];
|
||||
var unit = parsed[2].trim();
|
||||
|
||||
if (unit == "") return parseFloat(number);
|
||||
|
||||
var units = {
|
||||
b: 1,
|
||||
byte: 1,
|
||||
bytes: 1,
|
||||
kb: 1024,
|
||||
mb: Math.pow(1024, 2),
|
||||
gb: Math.pow(1024, 3),
|
||||
tb: Math.pow(1024, 4)
|
||||
};
|
||||
unit = unit.toLowerCase();
|
||||
|
||||
if (!units.hasOwnProperty(unit)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var factor = units[unit];
|
||||
return number * factor;
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
if (!seconds) return "-";
|
||||
if (seconds < 0) return "00:00:00";
|
||||
|
|
@ -698,3 +726,17 @@ function callViewModelsIf(allViewModels, method, condition, callback) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
var sizeObservable = function(observable) {
|
||||
return ko.computed({
|
||||
read: function() {
|
||||
return formatSize(observable());
|
||||
},
|
||||
write: function(value) {
|
||||
var result = bytesFromSize(value);
|
||||
if (result != undefined) {
|
||||
observable(result);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ $(function() {
|
|||
function GcodeFilesViewModel(parameters) {
|
||||
var self = this;
|
||||
|
||||
self.printerState = parameters[0];
|
||||
self.settingsViewModel = parameters[0];
|
||||
self.loginState = parameters[1];
|
||||
self.slicing = parameters[2];
|
||||
self.printerState = parameters[2];
|
||||
self.slicing = parameters[3];
|
||||
|
||||
self.isErrorOrClosed = ko.observable(undefined);
|
||||
self.isOperational = ko.observable(undefined);
|
||||
|
|
@ -21,11 +22,35 @@ $(function() {
|
|||
});
|
||||
|
||||
self.freeSpace = ko.observable(undefined);
|
||||
self.totalSpace = ko.observable(undefined);
|
||||
self.freeSpaceString = ko.computed(function() {
|
||||
if (!self.freeSpace())
|
||||
return "-";
|
||||
return formatSize(self.freeSpace());
|
||||
});
|
||||
self.totalSpaceString = ko.computed(function() {
|
||||
if (!self.totalSpace())
|
||||
return "-";
|
||||
return formatSize(self.totalSpace());
|
||||
});
|
||||
|
||||
self.diskusageWarning = ko.computed(function() {
|
||||
return self.freeSpace() != undefined
|
||||
&& self.freeSpace() < self.settingsViewModel.server_diskspace_warning();
|
||||
});
|
||||
self.diskusageCritical = ko.computed(function() {
|
||||
return self.freeSpace() != undefined
|
||||
&& self.freeSpace() < self.settingsViewModel.server_diskspace_critical();
|
||||
});
|
||||
self.diskusageString = ko.computed(function() {
|
||||
if (self.diskusageCritical()) {
|
||||
return gettext("Your available free disk space is critically low.");
|
||||
} else if (self.diskusageWarning()) {
|
||||
return gettext("Your available free disk space is starting to run low.");
|
||||
} else {
|
||||
return gettext("Your current disk usage.");
|
||||
}
|
||||
});
|
||||
|
||||
self.uploadButton = undefined;
|
||||
|
||||
|
|
@ -155,10 +180,14 @@ $(function() {
|
|||
}
|
||||
}
|
||||
|
||||
if (response.free) {
|
||||
if (response.free != undefined) {
|
||||
self.freeSpace(response.free);
|
||||
}
|
||||
|
||||
if (response.total != undefined) {
|
||||
self.totalSpace(response.total);
|
||||
}
|
||||
|
||||
self.highlightFilename(self.printerState.filename());
|
||||
};
|
||||
|
||||
|
|
@ -575,7 +604,7 @@ $(function() {
|
|||
|
||||
OCTOPRINT_VIEWMODELS.push([
|
||||
GcodeFilesViewModel,
|
||||
["printerStateViewModel", "loginStateViewModel", "slicingViewModel"],
|
||||
["settingsViewModel", "loginStateViewModel", "printerStateViewModel", "slicingViewModel"],
|
||||
"#files_wrapper"
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ $(function() {
|
|||
self.feature_disableExternalHeatupDetection = ko.observable(undefined);
|
||||
self.feature_keyboardControl = ko.observable(undefined);
|
||||
self.feature_pollWatched = ko.observable(undefined);
|
||||
self.feature_ignoreIdenticalResends = ko.observable(undefined);
|
||||
|
||||
self.serial_port = ko.observable();
|
||||
self.serial_baudrate = ko.observable();
|
||||
|
|
@ -164,6 +165,11 @@ $(function() {
|
|||
self.server_commands_systemRestartCommand = ko.observable(undefined);
|
||||
self.server_commands_serverRestartCommand = ko.observable(undefined);
|
||||
|
||||
self.server_diskspace_warning = ko.observable();
|
||||
self.server_diskspace_critical = ko.observable();
|
||||
self.server_diskspace_warning_str = sizeObservable(self.server_diskspace_warning);
|
||||
self.server_diskspace_critical_str = sizeObservable(self.server_diskspace_critical);
|
||||
|
||||
self.settings = undefined;
|
||||
self.lastReceivedSettings = undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,13 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_ignoreIdenticalResends" id="settings-ignoreIdenticalResends"> {{ _('Ignore consecutive resend requests for the same line') }} <span class="label">{{ _('Repetier') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
|
|
@ -62,11 +69,4 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="checked: feature_swallowOkAfterResend" id="settings-swallowOkAfterResend"> {{ _('Swallow the first "ok" after a resend response') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<form class="form-horizontal">
|
||||
<h3>{{ _('Folders') }}</h3>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-folderUploads">{{ _('Upload Folder') }}</label>
|
||||
<div class="controls">
|
||||
|
|
@ -36,4 +38,21 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{{ _('Disk space thresholds') }}</h3>
|
||||
|
||||
<p>{{ _('If the free disk space falls below these thresholds, OctoPrint will warn the user.') }}</p>
|
||||
|
||||
<div class="control-group" title="{{ ('Threshold after which to warn about the remaining free space on disk.') }}">
|
||||
<label class="control-label" for="settings.serverDiskusageWarning">{{ _('Warning') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-mini text-right" data-bind="value: server_diskspace_warning_str">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="{{ ('Threshold after which to consider remaining free space on disk as critical.') }}">
|
||||
<label class="control-label" for="settings.serverDiskusageCritical">{{ _('Critical') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-mini text-right" data-bind="value: server_diskspace_critical_str">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@
|
|||
var SOCKJS_URI = "{{ url_for('index') }}" + "sockjs";
|
||||
var SOCKJS_DEBUG = CONFIG_DEBUG;
|
||||
// sockjs should define CLOSE_NORMAL for us, but they don't (from ws spec)
|
||||
var SOCKJS_CLOSE_NORMAL = 1000
|
||||
var SOCKJS_CLOSE_NORMAL = 1000;
|
||||
|
||||
var UI_API_KEY = "{{ uiApiKey }}";
|
||||
var VERSION = "{{ version }}";
|
||||
var DISPLAY_VERSION = "{{ display_version }}";
|
||||
var BRANCH = "{{ branch }}";
|
||||
var LOCALE = "{{ g.locale }}";
|
||||
var AVAILABLE_LOCALES = {{ locales|tojson }};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<label class="checkbox">
|
||||
<input type="checkbox" id="login_remember" data-bind="checked: loginState.loginRemember"> {{ _('Remember me') }}
|
||||
</label>
|
||||
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">{{ _('Login') }}</button>
|
||||
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: function() { loginState.login(); }">{{ _('Login') }}</button>
|
||||
</div>
|
||||
<ul id="login_dropdown_loggedin" class="hide" data-bind="css: {hide: !loginState.loggedIn(), 'dropdown-menu': loginState.loggedIn()}">
|
||||
<li><a href="#" id="usersettings_button" data-bind="click: function() { usersettings.show(); }">{{ _('User Settings') }}</a></li>
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@
|
|||
<div class="title" data-bind="text: name"></div>
|
||||
</script>
|
||||
</div>
|
||||
<div class="muted text-right">
|
||||
<small>{{ _('Free') }}: <span data-bind="text: freeSpaceString"></span></small>
|
||||
<div class="text-right muted" data-bind="attr: {title: diskusageString}, css: {'text-error': diskusageCritical}, style: {'font-weight': diskusageCritical() || diskusageWarning() ? 'bold' : 'normal'}">
|
||||
<small>{{ _('Free') }}: <span data-bind="text: freeSpaceString"></span> / {{ _('Total') }}: <span data-bind="text: totalSpaceString"></span> <i class="icon-exclamation-sign" data-bind="visible: diskusageWarning" style="display: none"></i></small>
|
||||
</div>
|
||||
<div style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<div class="row-fluid upload-buttons">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ from functools import wraps
|
|||
import warnings
|
||||
import contextlib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def warning_decorator_factory(warning_type):
|
||||
|
|
@ -198,29 +197,12 @@ def get_exception_string():
|
|||
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
|
||||
|
||||
|
||||
@deprecated("get_free_bytes has been deprecated and will be removed in the future",
|
||||
includedoc="Replaced by `psutil.disk_usage <http://pythonhosted.org/psutil/#psutil.disk_usage>`_.",
|
||||
since="1.2.5")
|
||||
def get_free_bytes(path):
|
||||
"""
|
||||
Retrieves the number of free bytes on the partition ``path`` is located at and returns it. Works on both Windows and
|
||||
Unix/Linux.
|
||||
|
||||
Taken from http://stackoverflow.com/a/2372171/2028598
|
||||
|
||||
Arguments:
|
||||
path (string): The path for which to check the remaining partition space.
|
||||
|
||||
Returns:
|
||||
int: The amount of bytes still left on the partition.
|
||||
"""
|
||||
|
||||
path = os.path.abspath(path)
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
freeBytes = ctypes.c_ulonglong(0)
|
||||
ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, None, ctypes.pointer(freeBytes))
|
||||
return freeBytes.value
|
||||
else:
|
||||
st = os.statvfs(path)
|
||||
return st.f_bavail * st.f_frsize
|
||||
import psutil
|
||||
return psutil.disk_usage(path).free
|
||||
|
||||
|
||||
def get_dos_filename(origin, existing_filenames=None, extension=None, **kwargs):
|
||||
|
|
|
|||
|
|
@ -249,6 +249,8 @@ class MachineCom(object):
|
|||
self._lastResendNumber = None
|
||||
self._currentResendCount = 0
|
||||
self._resendSwallowNextOk = False
|
||||
self._resendSwallowRepetitions = settings().getBoolean(["feature", "ignoreIdenticalResends"])
|
||||
self._resendSwallowRepetitionsCounter = 0
|
||||
self._checksum_requiring_commands = settings().get(["serial", "checksumRequiringCommands"])
|
||||
|
||||
self._clear_to_send = CountedEvent(max=10, name="comm.clear_to_send")
|
||||
|
|
@ -1484,9 +1486,18 @@ class MachineCom(object):
|
|||
self._currentResendCount += 1
|
||||
return
|
||||
|
||||
# If we ignore resend repetitions (Repetier firmware...), check if we
|
||||
# need to do this now. If the same line number has been requested we
|
||||
# already saw and resent, we'll ignore it up to <counter> times.
|
||||
if self._resendSwallowRepetitions and lineToResend == self._lastResendNumber and self._resendSwallowRepetitionsCounter > 0:
|
||||
self._logger.debug("Ignoring resend request for line %d, that is probably a repetition sent by the firmware to ensure it arrives, not a real request" % lineToResend)
|
||||
self._resendSwallowRepetitionsCounter -= 1
|
||||
return
|
||||
|
||||
self._resendDelta = resendDelta
|
||||
self._lastResendNumber = lineToResend
|
||||
self._currentResendCount = 0
|
||||
self._resendSwallowRepetitionsCounter = settings().getInt(["feature", "identicalResendsCountdown"])
|
||||
|
||||
if self._resendDelta > len(self._lastLines) or len(self._lastLines) == 0 or self._resendDelta < 0:
|
||||
self._errorValue = "Printer requested line %d but no sufficient history is available, can't resend" % lineToResend
|
||||
|
|
@ -1740,14 +1751,14 @@ class MachineCom(object):
|
|||
def _gcode_T_sent(self, cmd, cmd_type=None):
|
||||
toolMatch = regexes_parameters["intT"].search(cmd)
|
||||
if toolMatch:
|
||||
self._currentTool = int(toolMatch.group(1))
|
||||
self._currentTool = int(toolMatch.group("value"))
|
||||
|
||||
def _gcode_G0_sent(self, cmd, cmd_type=None):
|
||||
if 'Z' in cmd:
|
||||
match = regexes_parameters["floatZ"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
z = float(match.group(1))
|
||||
z = float(match.group("value"))
|
||||
if self._currentZ != z:
|
||||
self._currentZ = z
|
||||
self._callback.on_comm_z_change(z)
|
||||
|
|
@ -1764,11 +1775,11 @@ class MachineCom(object):
|
|||
toolNum = self._currentTool
|
||||
toolMatch = regexes_parameters["intT"].search(cmd)
|
||||
if toolMatch:
|
||||
toolNum = int(toolMatch.group(1))
|
||||
toolNum = int(toolMatch.group("value"))
|
||||
match = regexes_parameters["floatS"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
target = float(match.group(1))
|
||||
target = float(match.group("value"))
|
||||
if toolNum in self._temp.keys() and self._temp[toolNum] is not None and isinstance(self._temp[toolNum], tuple):
|
||||
(actual, oldTarget) = self._temp[toolNum]
|
||||
self._temp[toolNum] = (actual, target)
|
||||
|
|
@ -1781,7 +1792,7 @@ class MachineCom(object):
|
|||
match = regexes_parameters["floatS"].search(cmd)
|
||||
if match:
|
||||
try:
|
||||
target = float(match.group(1))
|
||||
target = float(match.group("value"))
|
||||
if self._bedTemp is not None and isinstance(self._bedTemp, tuple):
|
||||
(actual, oldTarget) = self._bedTemp
|
||||
self._bedTemp = (actual, target)
|
||||
|
|
|
|||
291
tests/static/js/lib/qunit-1.18.0.css
Normal file
291
tests/static/js/lib/qunit-1.18.0.css
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
/*!
|
||||
* QUnit 1.18.0
|
||||
* http://qunitjs.com/
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* Date: 2015-04-03T10:23Z
|
||||
*/
|
||||
|
||||
/** Font Family and Sizes */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
|
||||
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
|
||||
#qunit-tests { font-size: smaller; }
|
||||
|
||||
|
||||
/** Resets */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/** Header */
|
||||
|
||||
#qunit-header {
|
||||
padding: 0.5em 0 0.5em 1em;
|
||||
|
||||
color: #8699A4;
|
||||
background-color: #0D3349;
|
||||
|
||||
font-size: 1.5em;
|
||||
line-height: 1em;
|
||||
font-weight: 400;
|
||||
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
#qunit-header a {
|
||||
text-decoration: none;
|
||||
color: #C2CCD1;
|
||||
}
|
||||
|
||||
#qunit-header a:hover,
|
||||
#qunit-header a:focus {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar label {
|
||||
display: inline-block;
|
||||
padding: 0 0.5em 0 0.1em;
|
||||
}
|
||||
|
||||
#qunit-banner {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
color: #5E740B;
|
||||
background-color: #EEE;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#qunit-userAgent {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
background-color: #2B81AF;
|
||||
color: #FFF;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-container {
|
||||
float: right;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.qunit-url-config {
|
||||
display: inline-block;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
.qunit-filter {
|
||||
display: block;
|
||||
float: right;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
/** Tests: Pass/Fail */
|
||||
|
||||
#qunit-tests {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests li {
|
||||
padding: 0.4em 1em 0.4em 1em;
|
||||
border-bottom: 1px solid #FFF;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests > li {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests li.running,
|
||||
#qunit-tests li.pass,
|
||||
#qunit-tests li.fail,
|
||||
#qunit-tests li.skipped {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
#qunit-tests.hidepass li.running,
|
||||
#qunit-tests.hidepass li.pass {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#qunit-tests li strong {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#qunit-tests li.skipped strong {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#qunit-tests li a {
|
||||
padding: 0.5em;
|
||||
color: #C2CCD1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-tests li p a {
|
||||
padding: 0.25em;
|
||||
color: #6B6464;
|
||||
}
|
||||
#qunit-tests li a:hover,
|
||||
#qunit-tests li a:focus {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#qunit-tests li .runtime {
|
||||
float: right;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.qunit-assert-list {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
background-color: #FFF;
|
||||
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.qunit-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests table {
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
#qunit-tests th {
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
padding: 0 0.5em 0 0;
|
||||
}
|
||||
|
||||
#qunit-tests td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#qunit-tests pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#qunit-tests del {
|
||||
background-color: #E0F2BE;
|
||||
color: #374E0C;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-tests ins {
|
||||
background-color: #FFCACA;
|
||||
color: #500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*** Test Counts */
|
||||
|
||||
#qunit-tests b.counts { color: #000; }
|
||||
#qunit-tests b.passed { color: #5E740B; }
|
||||
#qunit-tests b.failed { color: #710909; }
|
||||
|
||||
#qunit-tests li li {
|
||||
padding: 5px;
|
||||
background-color: #FFF;
|
||||
border-bottom: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/*** Passing Styles */
|
||||
|
||||
#qunit-tests li li.pass {
|
||||
color: #3C510C;
|
||||
background-color: #FFF;
|
||||
border-left: 10px solid #C6E746;
|
||||
}
|
||||
|
||||
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
|
||||
#qunit-tests .pass .test-name { color: #366097; }
|
||||
|
||||
#qunit-tests .pass .test-actual,
|
||||
#qunit-tests .pass .test-expected { color: #999; }
|
||||
|
||||
#qunit-banner.qunit-pass { background-color: #C6E746; }
|
||||
|
||||
/*** Failing Styles */
|
||||
|
||||
#qunit-tests li li.fail {
|
||||
color: #710909;
|
||||
background-color: #FFF;
|
||||
border-left: 10px solid #EE5757;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#qunit-tests > li:last-child {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
#qunit-tests .fail { color: #000; background-color: #EE5757; }
|
||||
#qunit-tests .fail .test-name,
|
||||
#qunit-tests .fail .module-name { color: #000; }
|
||||
|
||||
#qunit-tests .fail .test-actual { color: #EE5757; }
|
||||
#qunit-tests .fail .test-expected { color: #008000; }
|
||||
|
||||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
||||
|
||||
/*** Skipped tests */
|
||||
|
||||
#qunit-tests .skipped {
|
||||
background-color: #EBECE9;
|
||||
}
|
||||
|
||||
#qunit-tests .qunit-skipped-label {
|
||||
background-color: #F4FF77;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
color: #366097;
|
||||
line-height: 1.8em;
|
||||
padding: 0 0.5em;
|
||||
margin: -0.4em 0.4em -0.4em 0;
|
||||
}
|
||||
|
||||
/** Result */
|
||||
|
||||
#qunit-testresult {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
|
||||
color: #2B81AF;
|
||||
background-color: #D2E0E6;
|
||||
|
||||
border-bottom: 1px solid #FFF;
|
||||
}
|
||||
#qunit-testresult .module-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/** Fixture */
|
||||
|
||||
#qunit-fixture {
|
||||
position: absolute;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
width: 1000px;
|
||||
height: 1000px;
|
||||
}
|
||||
3828
tests/static/js/lib/qunit-1.18.0.js
Normal file
3828
tests/static/js/lib/qunit-1.18.0.js
Normal file
File diff suppressed because it is too large
Load diff
133
tests/static/js/lib/qunit-parameterize.js
Normal file
133
tests/static/js/lib/qunit-parameterize.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Parameterize v 0.4
|
||||
* A QUnit Addon For Running Parameterized Tests
|
||||
* https://github.com/AStepaniuk/qunit-parameterize
|
||||
* Released under the MIT license.
|
||||
*/
|
||||
QUnit.extend(QUnit, {
|
||||
cases : function(testCases) {
|
||||
var currentCases = testCases;
|
||||
|
||||
var createTest = function(methodName, title, expected, callback, parameters) {
|
||||
QUnit[methodName](
|
||||
title,
|
||||
expected,
|
||||
function(assert) { return callback.call(this, parameters, assert); }
|
||||
);
|
||||
};
|
||||
|
||||
var iterateTestCases = function(methodName, title, expected, callback) {
|
||||
if (!currentCases || currentCases.length == 0) {
|
||||
// setup test which will always fail
|
||||
QUnit.test(title, function(assert) {
|
||||
assert.ok(false, "No test cases are provided");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = expected;
|
||||
expected = null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < currentCases.length; ++i) {
|
||||
var parameters = currentCases[i];
|
||||
|
||||
var testCaseTitle = title;
|
||||
if (parameters.title) {
|
||||
testCaseTitle += "[" + parameters.title + "]";
|
||||
}
|
||||
|
||||
createTest(methodName, testCaseTitle, expected, callback, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
var getLength = function(arr) {
|
||||
return arr ? arr.length : 0;
|
||||
}
|
||||
|
||||
var getItem = function(arr, idx) {
|
||||
return arr ? arr[idx] : undefined;
|
||||
}
|
||||
|
||||
var mix = function(testCase, mixData) {
|
||||
if (testCase && mixData) {
|
||||
var result = clone(testCase);
|
||||
for(var p in mixData) {
|
||||
if (p !== "title") {
|
||||
if (!(p in result)) result[p] = mixData[p];
|
||||
} else {
|
||||
result[p] = [result[p], mixData[p]].join("");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else if (testCase) {
|
||||
return testCase;
|
||||
} else if (mixData) {
|
||||
return mixData;
|
||||
} else {
|
||||
// return null or undefined whatever testCase is
|
||||
return testCase;
|
||||
}
|
||||
}
|
||||
|
||||
var clone = function(testCase) {
|
||||
var result = {};
|
||||
for (var p in testCase) {
|
||||
result[p] = testCase[p];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
sequential : function(addData) {
|
||||
var casesLength = getLength(currentCases);
|
||||
var addDataLength = getLength(addData);
|
||||
var length = casesLength > addDataLength ? casesLength : addDataLength;
|
||||
|
||||
var newCases = [];
|
||||
for (var i = 0; i < length; ++i) {
|
||||
var currentCaseI = getItem(currentCases, i);
|
||||
var dataI = getItem(addData, i);
|
||||
var newCase = mix(currentCaseI, dataI);
|
||||
|
||||
if (newCase) newCases.push(newCase);
|
||||
}
|
||||
currentCases = newCases;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
combinatorial : function(mixData) {
|
||||
var current = (currentCases && currentCases.length > 0) ? currentCases : [ null ];
|
||||
mixData = (mixData && mixData.length > 0) ? mixData : [ null ];
|
||||
var currentLength = current.length;
|
||||
var mixDataLength = mixData.length;
|
||||
|
||||
var newCases = [];
|
||||
for (var i = 0; i < currentLength; ++i) {
|
||||
for(var j = 0; j < mixDataLength; ++j) {
|
||||
var currentCaseI = current[i];
|
||||
var dataJ = mixData[j];
|
||||
var newCase = mix(currentCaseI, dataJ);
|
||||
|
||||
if (newCase) newCases.push(newCase);
|
||||
}
|
||||
}
|
||||
currentCases = newCases;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
test : function(title, expected, callback) {
|
||||
iterateTestCases("test", title, expected, callback);
|
||||
return this;
|
||||
},
|
||||
|
||||
asyncTest : function(title, expected, callback) {
|
||||
iterateTestCases("asyncTest", title, expected, callback);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
18
tests/static/js/test-helpers.html
Normal file
18
tests/static/js/test-helpers.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>src/octoprint/static/js/app/helpers.js Tests</title>
|
||||
<link rel="stylesheet" href="lib/qunit-1.18.0.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="qunit"></div>
|
||||
<div id="qunit-fixture"></div>
|
||||
<script src="lib/qunit-1.18.0.js"></script>
|
||||
<script src="lib/qunit-parameterize.js"></script>
|
||||
<script src="../../../src/octoprint/static/js/lib/lodash.min.js"></script>
|
||||
<script src="../../../src/octoprint/static/js/lib/sprintf.min.js"></script>
|
||||
<script src="../../../src/octoprint/static/js/app/helpers.js"></script>
|
||||
<script src="test-helpers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
80
tests/static/js/test-helpers.js
Normal file
80
tests/static/js/test-helpers.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
_.mixin({"sprintf": sprintf, "vsprintf": vsprintf});
|
||||
|
||||
QUnit.module("bytesFromSize");
|
||||
QUnit
|
||||
.cases(function(){
|
||||
var cases = [];
|
||||
|
||||
var params = [
|
||||
// empty inputs
|
||||
{ input: undefined, expected: undefined },
|
||||
{ input: "", expected: undefined },
|
||||
|
||||
// unknown units
|
||||
{ input: "1 PB", expected: undefined },
|
||||
{ input: "234.5 unknown", expected: undefined },
|
||||
{ input: "234.5unknown", expected: undefined },
|
||||
|
||||
// conversion
|
||||
{ input: "1", expected: 1 },
|
||||
{ input: "1b", expected: 1 },
|
||||
{ input: "1 B", expected: 1 },
|
||||
{ input: "1 byte", expected: 1 },
|
||||
{ input: "1 bYtES", expected: 1 },
|
||||
{ input: "1.1", expected: 1.1 },
|
||||
{ input: ".1", expected: 0.1 },
|
||||
{ input: "1 KB", expected: 1024 },
|
||||
{ input: "2 KB", expected: 2048 },
|
||||
{ input: "1 MB", expected: Math.pow(1024, 2)},
|
||||
{ input: "500mb", expected: 500 * Math.pow(1024, 2)},
|
||||
{ input: "500.2mb", expected: 500.2 * Math.pow(1024, 2)},
|
||||
{ input: "1 GB", expected: Math.pow(1024, 3)},
|
||||
{ input: "1 TB", expected: Math.pow(1024, 4)}
|
||||
];
|
||||
|
||||
var param, i;
|
||||
for (i = 0; i < params.length; i++) {
|
||||
param = params[i];
|
||||
param["title"] = param.input != undefined ? '"' + String(param.input) + '"' : "undefined";
|
||||
cases.push(param);
|
||||
}
|
||||
|
||||
return cases;
|
||||
}())
|
||||
.test("bytesFromSize", function(params, assert) {
|
||||
assert.equal(params.expected, bytesFromSize(params.input), "As expected: " + String(params.expected));
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.module("formatSize");
|
||||
QUnit
|
||||
.cases(function(){
|
||||
var cases = [];
|
||||
|
||||
var params = [
|
||||
{ input: undefined, expected: "-" },
|
||||
{ input: "", expected: "-" },
|
||||
{ input: 1, expected: "1.0bytes" },
|
||||
{ input: 1.1, expected: "1.1bytes" },
|
||||
{ input: 1024, expected: "1.0KB" },
|
||||
{ input: 2048, expected: "2.0KB" },
|
||||
{ input: 2.2 * 1024, expected: "2.2KB" },
|
||||
{ input: 23.5 * Math.pow(1024, 2), expected: "23.5MB" },
|
||||
{ input: 23.5 * Math.pow(1024, 3), expected: "23.5GB" },
|
||||
{ input: 23.5 * Math.pow(1024, 4), expected: "23.5TB" },
|
||||
{ input: 2 * Math.pow(1024, 5), expected: "2048.0TB" }
|
||||
];
|
||||
|
||||
var param, i;
|
||||
for (i = 0; i < params.length; i++) {
|
||||
param = params[i];
|
||||
param["title"] = String(param.input);
|
||||
cases.push(param);
|
||||
}
|
||||
|
||||
return cases;
|
||||
}())
|
||||
.test("formatSize", function(params, assert) {
|
||||
assert.equal(params.expected, formatSize(params.input), "As expected: " + String(params.expected));
|
||||
}
|
||||
);
|
||||
Loading…
Reference in a new issue