Merge branch 'blacklist-history'
This commit is contained in:
commit
2fcdc5a0c9
@ -25,6 +25,8 @@ Added
|
|||||||
- When `:spawn --userscript` is called with a count, that count is now
|
- When `:spawn --userscript` is called with a count, that count is now
|
||||||
passed to userscripts as `$QUTE_COUNT`.
|
passed to userscripts as `$QUTE_COUNT`.
|
||||||
- New `content.mouse_lock` setting to handle HTML5 pointer locking.
|
- New `content.mouse_lock` setting to handle HTML5 pointer locking.
|
||||||
|
- New `completion.web_history.exclude` setting which hides a list of URL
|
||||||
|
patterns from the completion.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@ -39,6 +41,8 @@ Changed
|
|||||||
on macOS.
|
on macOS.
|
||||||
- Using `:set option` now shows the value of the setting (like `:set option?`
|
- Using `:set option` now shows the value of the setting (like `:set option?`
|
||||||
already did).
|
already did).
|
||||||
|
- The `completion.web_history_max_items` setting got renamed to
|
||||||
|
`completion.web_history.max_items`.
|
||||||
|
|
||||||
v1.4.2
|
v1.4.2
|
||||||
------
|
------
|
||||||
|
@ -453,7 +453,7 @@ or always navigate through command history with
|
|||||||
:bind -m command <Down> command-history-next
|
:bind -m command <Down> command-history-next
|
||||||
----
|
----
|
||||||
|
|
||||||
- The default for `completion.web_history_max_items` is now set to `-1`, showing
|
- The default for `completion.web_history.max_items` is now set to `-1`, showing
|
||||||
an unlimited number of items in the completion for `:open` as the new
|
an unlimited number of items in the completion for `:open` as the new
|
||||||
sqlite-based completion is much faster. If the `:open` completion is too slow
|
sqlite-based completion is much faster. If the `:open` completion is too slow
|
||||||
on your machine, set an appropriate limit again.
|
on your machine, set an appropriate limit again.
|
||||||
|
@ -107,7 +107,8 @@
|
|||||||
|<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars.
|
|<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars.
|
||||||
|<<completion.timestamp_format,completion.timestamp_format>>|Format of timestamps (e.g. for the history completion).
|
|<<completion.timestamp_format,completion.timestamp_format>>|Format of timestamps (e.g. for the history completion).
|
||||||
|<<completion.use_best_match,completion.use_best_match>>|Execute the best-matching command on a partial match.
|
|<<completion.use_best_match,completion.use_best_match>>|Execute the best-matching command on a partial match.
|
||||||
|<<completion.web_history_max_items,completion.web_history_max_items>>|Number of URLs to show in the web history.
|
|<<completion.web_history.exclude,completion.web_history.exclude>>|A list of patterns which should not be shown in the history.
|
||||||
|
|<<completion.web_history.max_items,completion.web_history.max_items>>|Number of URLs to show in the web history.
|
||||||
|<<confirm_quit,confirm_quit>>|Require a confirmation before quitting the application.
|
|<<confirm_quit,confirm_quit>>|Require a confirmation before quitting the application.
|
||||||
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|
|<<content.autoplay,content.autoplay>>|Automatically start playing `<video>` elements.
|
||||||
|<<content.cache.appcache,content.cache.appcache>>|Enable support for the HTML 5 web application cache feature.
|
|<<content.cache.appcache,content.cache.appcache>>|Enable support for the HTML 5 web application cache feature.
|
||||||
@ -1447,8 +1448,19 @@ Type: <<types,Bool>>
|
|||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
|
|
||||||
[[completion.web_history_max_items]]
|
[[completion.web_history.exclude]]
|
||||||
=== completion.web_history_max_items
|
=== completion.web_history.exclude
|
||||||
|
A list of patterns which should not be shown in the history.
|
||||||
|
This only affects the completion. Matching URLs are still saved in the history (and visible on the qute://history page), but hidden in the completion.
|
||||||
|
Changing this setting will cause the completion history to be regenerated on the next start, which will take a short while.
|
||||||
|
This setting requires a restart.
|
||||||
|
|
||||||
|
Type: <<types,List of UrlPattern>>
|
||||||
|
|
||||||
|
Default: empty
|
||||||
|
|
||||||
|
[[completion.web_history.max_items]]
|
||||||
|
=== completion.web_history.max_items
|
||||||
Number of URLs to show in the web history.
|
Number of URLs to show in the web history.
|
||||||
0: no history / -1: unlimited
|
0: no history / -1: unlimited
|
||||||
|
|
||||||
@ -3449,5 +3461,8 @@ See the setting's valid values for more information on allowed values.
|
|||||||
See https://sqlite.org/lang_datefunc.html for reference.
|
See https://sqlite.org/lang_datefunc.html for reference.
|
||||||
|UniqueCharString|A string which may not contain duplicate chars.
|
|UniqueCharString|A string which may not contain duplicate chars.
|
||||||
|Url|A URL as a string.
|
|Url|A URL as a string.
|
||||||
|
|UrlPattern|A match pattern for a URL.
|
||||||
|
|
||||||
|
See https://developer.chrome.com/apps/match_patterns for the allowed syntax.
|
||||||
|VerticalPosition|The position of the download bar.
|
|VerticalPosition|The position of the download bar.
|
||||||
|==============
|
|==============
|
||||||
|
@ -449,13 +449,10 @@ def _init_modules(args, crash_handler):
|
|||||||
|
|
||||||
log.init.debug("Initializing web history...")
|
log.init.debug("Initializing web history...")
|
||||||
history.init(qApp)
|
history.init(qApp)
|
||||||
except sql.SqlError as e:
|
except sql.SqlEnvironmentError as e:
|
||||||
if e.environmental:
|
|
||||||
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
error.handle_fatal_exc(e, args, 'Error initializing SQL',
|
||||||
pre_text='Error initializing SQL')
|
pre_text='Error initializing SQL')
|
||||||
sys.exit(usertypes.Exit.err_init)
|
sys.exit(usertypes.Exit.err_init)
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
log.init.debug("Initializing completion...")
|
log.init.debug("Initializing completion...")
|
||||||
completiondelegate.init()
|
completiondelegate.init()
|
||||||
|
@ -25,6 +25,7 @@ import contextlib
|
|||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer, pyqtSignal
|
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer, pyqtSignal
|
||||||
|
|
||||||
|
from qutebrowser.config import config
|
||||||
from qutebrowser.commands import cmdutils, cmdexc
|
from qutebrowser.commands import cmdutils, cmdexc
|
||||||
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
|
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
|
||||||
debug, standarddir, qtutils)
|
debug, standarddir, qtutils)
|
||||||
@ -35,6 +36,41 @@ from qutebrowser.misc import objects, sql
|
|||||||
_USER_VERSION = 2
|
_USER_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionMetaInfo(sql.SqlTable):
|
||||||
|
|
||||||
|
"""Table containing meta-information for the completion."""
|
||||||
|
|
||||||
|
KEYS = {
|
||||||
|
'force_rebuild': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__("CompletionMetaInfo", ['key', 'value'],
|
||||||
|
constraints={'key': 'PRIMARY KEY'})
|
||||||
|
for key, default in self.KEYS.items():
|
||||||
|
if key not in self:
|
||||||
|
self[key] = default
|
||||||
|
|
||||||
|
def _check_key(self, key):
|
||||||
|
if key not in self.KEYS:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
self._check_key(key)
|
||||||
|
query = self.contains_query('key')
|
||||||
|
return query.run(val=key).value()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
self._check_key(key)
|
||||||
|
query = sql.Query('SELECT value FROM CompletionMetaInfo '
|
||||||
|
'WHERE key = :key')
|
||||||
|
return query.run(key=key).value()
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._check_key(key)
|
||||||
|
self.insert({'key': key, 'value': value}, replace=True)
|
||||||
|
|
||||||
|
|
||||||
class CompletionHistory(sql.SqlTable):
|
class CompletionHistory(sql.SqlTable):
|
||||||
|
|
||||||
"""History which only has the newest entry for each URL."""
|
"""History which only has the newest entry for each URL."""
|
||||||
@ -65,11 +101,18 @@ class WebHistory(sql.SqlTable):
|
|||||||
'redirect': 'NOT NULL'},
|
'redirect': 'NOT NULL'},
|
||||||
parent=parent)
|
parent=parent)
|
||||||
self.completion = CompletionHistory(parent=self)
|
self.completion = CompletionHistory(parent=self)
|
||||||
|
self.metainfo = CompletionMetaInfo(parent=self)
|
||||||
|
|
||||||
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
|
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
|
||||||
self.completion.delete_all()
|
self.completion.delete_all()
|
||||||
|
if self.metainfo['force_rebuild']:
|
||||||
|
self.completion.delete_all()
|
||||||
|
self.metainfo['force_rebuild'] = False
|
||||||
|
|
||||||
if not self.completion:
|
if not self.completion:
|
||||||
# either the table is out-of-date or the user wiped it manually
|
# either the table is out-of-date or the user wiped it manually
|
||||||
self._rebuild_completion()
|
self._rebuild_completion()
|
||||||
|
|
||||||
self.create_index('HistoryIndex', 'url')
|
self.create_index('HistoryIndex', 'url')
|
||||||
self.create_index('HistoryAtimeIndex', 'atime')
|
self.create_index('HistoryAtimeIndex', 'atime')
|
||||||
self._contains_query = self.contains_query('url')
|
self._contains_query = self.contains_query('url')
|
||||||
@ -87,21 +130,29 @@ class WebHistory(sql.SqlTable):
|
|||||||
'ORDER BY atime desc '
|
'ORDER BY atime desc '
|
||||||
'limit :limit offset :offset')
|
'limit :limit offset :offset')
|
||||||
|
|
||||||
|
config.instance.changed.connect(self._on_config_changed)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, length=len(self))
|
return utils.get_repr(self, length=len(self))
|
||||||
|
|
||||||
def __contains__(self, url):
|
def __contains__(self, url):
|
||||||
return self._contains_query.run(val=url).value()
|
return self._contains_query.run(val=url).value()
|
||||||
|
|
||||||
|
@config.change_filter('completion.web_history.exclude')
|
||||||
|
def _on_config_changed(self):
|
||||||
|
self.metainfo['force_rebuild'] = True
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _handle_sql_errors(self):
|
def _handle_sql_errors(self):
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except sql.SqlError as e:
|
except sql.SqlEnvironmentError as e:
|
||||||
if e.environmental:
|
|
||||||
message.error("Failed to write history: {}".format(e.text()))
|
message.error("Failed to write history: {}".format(e.text()))
|
||||||
else:
|
|
||||||
raise
|
def _is_excluded(self, url):
|
||||||
|
"""Check if the given URL is excluded from the completion."""
|
||||||
|
return any(pattern.matches(url)
|
||||||
|
for pattern in config.val.completion.web_history.exclude)
|
||||||
|
|
||||||
def _rebuild_completion(self):
|
def _rebuild_completion(self):
|
||||||
data = {'url': [], 'title': [], 'last_atime': []}
|
data = {'url': [], 'title': [], 'last_atime': []}
|
||||||
@ -110,9 +161,13 @@ class WebHistory(sql.SqlTable):
|
|||||||
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
|
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
|
||||||
'GROUP BY url ORDER BY atime asc')
|
'GROUP BY url ORDER BY atime asc')
|
||||||
for entry in q.run():
|
for entry in q.run():
|
||||||
data['url'].append(self._format_completion_url(QUrl(entry.url)))
|
url = QUrl(entry.url)
|
||||||
|
if self._is_excluded(url):
|
||||||
|
continue
|
||||||
|
data['url'].append(self._format_completion_url(url))
|
||||||
data['title'].append(entry.title)
|
data['title'].append(entry.title)
|
||||||
data['last_atime'].append(entry.atime)
|
data['last_atime'].append(entry.atime)
|
||||||
|
|
||||||
self.completion.insert_batch(data, replace=True)
|
self.completion.insert_batch(data, replace=True)
|
||||||
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
|
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
|
||||||
|
|
||||||
@ -218,7 +273,10 @@ class WebHistory(sql.SqlTable):
|
|||||||
'title': title,
|
'title': title,
|
||||||
'atime': atime,
|
'atime': atime,
|
||||||
'redirect': redirect})
|
'redirect': redirect})
|
||||||
if not redirect:
|
|
||||||
|
if redirect or self._is_excluded(url):
|
||||||
|
return
|
||||||
|
|
||||||
self.completion.insert({
|
self.completion.insert({
|
||||||
'url': self._format_completion_url(url),
|
'url': self._format_completion_url(url),
|
||||||
'title': title,
|
'title': title,
|
||||||
|
@ -42,7 +42,7 @@ class HistoryCategory(QSqlQueryModel):
|
|||||||
|
|
||||||
def _atime_expr(self):
|
def _atime_expr(self):
|
||||||
"""If max_items is set, return an expression to limit the query."""
|
"""If max_items is set, return an expression to limit the query."""
|
||||||
max_items = config.val.completion.web_history_max_items
|
max_items = config.val.completion.web_history.max_items
|
||||||
# HistoryCategory should not be added to the completion in that case.
|
# HistoryCategory should not be added to the completion in that case.
|
||||||
assert max_items != 0
|
assert max_items != 0
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class HistoryCategory(QSqlQueryModel):
|
|||||||
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
|
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
|
||||||
.format(timestamp_format.replace("'", "`")))
|
.format(timestamp_format.replace("'", "`")))
|
||||||
|
|
||||||
if not self._query or len(words) != len(self._query.boundValues()):
|
if not self._query or len(words) != len(self._query.bound_values()):
|
||||||
# if the number of words changed, we need to generate a new query
|
# if the number of words changed, we need to generate a new query
|
||||||
# otherwise, we can reuse the prepared query for performance
|
# otherwise, we can reuse the prepared query for performance
|
||||||
self._query = sql.Query(' '.join([
|
self._query = sql.Query(' '.join([
|
||||||
@ -100,14 +100,14 @@ class HistoryCategory(QSqlQueryModel):
|
|||||||
with debug.log_time('sql', 'Running completion query'):
|
with debug.log_time('sql', 'Running completion query'):
|
||||||
self._query.run(**{
|
self._query.run(**{
|
||||||
str(i): w for i, w in enumerate(words)})
|
str(i): w for i, w in enumerate(words)})
|
||||||
self.setQuery(self._query)
|
self.setQuery(self._query.query)
|
||||||
|
|
||||||
def removeRows(self, row, _count, _parent=None):
|
def removeRows(self, row, _count, _parent=None):
|
||||||
"""Override QAbstractItemModel::removeRows to re-run sql query."""
|
"""Override QAbstractItemModel::removeRows to re-run sql query."""
|
||||||
# re-run query to reload updated table
|
# re-run query to reload updated table
|
||||||
with debug.log_time('sql', 'Re-running completion query post-delete'):
|
with debug.log_time('sql', 'Re-running completion query post-delete'):
|
||||||
self._query.run()
|
self._query.run()
|
||||||
self.setQuery(self._query)
|
self.setQuery(self._query.query)
|
||||||
while self.rowCount() < row:
|
while self.rowCount() < row:
|
||||||
self.fetchMore()
|
self.fetchMore()
|
||||||
return True
|
return True
|
||||||
|
@ -68,7 +68,7 @@ def url(*, info):
|
|||||||
model.add_category(listcategory.ListCategory(
|
model.add_category(listcategory.ListCategory(
|
||||||
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
|
'Bookmarks', bookmarks, delete_func=_delete_bookmark, sort=False))
|
||||||
|
|
||||||
if info.config.get('completion.web_history_max_items') != 0:
|
if info.config.get('completion.web_history.max_items') != 0:
|
||||||
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
|
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
|
||||||
model.add_category(hist_cat)
|
model.add_category(hist_cat)
|
||||||
return model
|
return model
|
||||||
|
@ -812,7 +812,27 @@ completion.timestamp_format:
|
|||||||
default: '%Y-%m-%d'
|
default: '%Y-%m-%d'
|
||||||
desc: Format of timestamps (e.g. for the history completion).
|
desc: Format of timestamps (e.g. for the history completion).
|
||||||
|
|
||||||
|
completion.web_history.exclude:
|
||||||
|
type:
|
||||||
|
name: List
|
||||||
|
valtype: UrlPattern
|
||||||
|
none_ok: true
|
||||||
|
default: []
|
||||||
|
restart: true
|
||||||
|
desc: >-
|
||||||
|
A list of patterns which should not be shown in the history.
|
||||||
|
|
||||||
|
This only affects the completion. Matching URLs are still saved in the
|
||||||
|
history (and visible on the qute://history page), but hidden in the
|
||||||
|
completion.
|
||||||
|
|
||||||
|
Changing this setting will cause the completion history to be regenerated
|
||||||
|
on the next start, which will take a short while.
|
||||||
|
|
||||||
completion.web_history_max_items:
|
completion.web_history_max_items:
|
||||||
|
renamed: completion.web_history.max_items
|
||||||
|
|
||||||
|
completion.web_history.max_items:
|
||||||
default: -1
|
default: -1
|
||||||
type:
|
type:
|
||||||
name: Int
|
name: Int
|
||||||
|
@ -61,7 +61,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
|
|||||||
|
|
||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
from qutebrowser.config import configexc
|
from qutebrowser.config import configexc
|
||||||
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
|
from qutebrowser.utils import standarddir, utils, qtutils, urlutils, urlmatch
|
||||||
from qutebrowser.keyinput import keyutils
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
|
|
||||||
@ -1661,3 +1661,22 @@ class Key(BaseType):
|
|||||||
return keyutils.KeySequence.parse(value)
|
return keyutils.KeySequence.parse(value)
|
||||||
except keyutils.KeyParseError as e:
|
except keyutils.KeyParseError as e:
|
||||||
raise configexc.ValidationError(value, str(e))
|
raise configexc.ValidationError(value, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class UrlPattern(BaseType):
|
||||||
|
|
||||||
|
"""A match pattern for a URL.
|
||||||
|
|
||||||
|
See https://developer.chrome.com/apps/match_patterns for the allowed
|
||||||
|
syntax.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_py(self, value):
|
||||||
|
self._basic_py_validation(value, str)
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return urlmatch.UrlPattern(value)
|
||||||
|
except urlmatch.ParseError as e:
|
||||||
|
raise configexc.ValidationError(value, str(e))
|
||||||
|
@ -27,51 +27,78 @@ from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlError
|
|||||||
from qutebrowser.utils import log, debug
|
from qutebrowser.utils import log, debug
|
||||||
|
|
||||||
|
|
||||||
|
class SqliteErrorCode:
|
||||||
|
|
||||||
|
"""Error codes as used by sqlite.
|
||||||
|
|
||||||
|
See https://sqlite.org/rescode.html - note we only define the codes we use
|
||||||
|
in qutebrowser here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
BUSY = '5' # database is locked
|
||||||
|
READONLY = '8' # attempt to write a readonly database
|
||||||
|
IOERR = '10' # disk I/O error
|
||||||
|
CORRUPT = '11' # database disk image is malformed
|
||||||
|
FULL = '13' # database or disk is full
|
||||||
|
CANTOPEN = '14' # unable to open database file
|
||||||
|
CONSTRAINT = '19' # UNIQUE constraint failed
|
||||||
|
|
||||||
|
|
||||||
class SqlError(Exception):
|
class SqlError(Exception):
|
||||||
|
|
||||||
"""Raised on an error interacting with the SQL database.
|
"""Base class for all SQL related errors."""
|
||||||
|
|
||||||
Attributes:
|
def __init__(self, msg, error=None):
|
||||||
environmental: Whether the error is likely caused by the environment
|
|
||||||
and not a qutebrowser bug.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, msg, environmental=False):
|
|
||||||
super().__init__(msg)
|
|
||||||
self.environmental = environmental
|
|
||||||
|
|
||||||
def text(self):
|
|
||||||
"""Get a short text to display."""
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
class SqliteError(SqlError):
|
|
||||||
|
|
||||||
"""A SQL error with a QSqlError available.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
error: The QSqlError object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, msg, error):
|
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
self.error = error
|
self.error = error
|
||||||
|
|
||||||
|
def text(self):
|
||||||
|
"""Get a short text description of the error.
|
||||||
|
|
||||||
|
This is a string suitable to show to the user as error message.
|
||||||
|
"""
|
||||||
|
if self.error is None:
|
||||||
|
return str(self)
|
||||||
|
else:
|
||||||
|
return self.error.databaseText()
|
||||||
|
|
||||||
|
|
||||||
|
class SqlEnvironmentError(SqlError):
|
||||||
|
|
||||||
|
"""Raised on an error interacting with the SQL database.
|
||||||
|
|
||||||
|
This is raised in conditions resulting from the environment (like a full
|
||||||
|
disk or I/O errors), where qutebrowser isn't to blame.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SqlBugError(SqlError):
|
||||||
|
|
||||||
|
"""Raised on an error interacting with the SQL database.
|
||||||
|
|
||||||
|
This is raised for errors resulting from a qutebrowser bug.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def raise_sqlite_error(msg, error):
|
||||||
|
"""Raise either a SqlBugError or SqlEnvironmentError."""
|
||||||
log.sql.debug("SQL error:")
|
log.sql.debug("SQL error:")
|
||||||
log.sql.debug("type: {}".format(
|
log.sql.debug("type: {}".format(
|
||||||
debug.qenum_key(QSqlError, error.type())))
|
debug.qenum_key(QSqlError, error.type())))
|
||||||
log.sql.debug("database text: {}".format(error.databaseText()))
|
log.sql.debug("database text: {}".format(error.databaseText()))
|
||||||
log.sql.debug("driver text: {}".format(error.driverText()))
|
log.sql.debug("driver text: {}".format(error.driverText()))
|
||||||
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
|
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
|
||||||
|
|
||||||
# https://sqlite.org/rescode.html
|
|
||||||
# https://github.com/qutebrowser/qutebrowser/issues/2930
|
|
||||||
# https://github.com/qutebrowser/qutebrowser/issues/3004
|
|
||||||
environmental_errors = [
|
environmental_errors = [
|
||||||
'5', # SQLITE_BUSY ("database is locked")
|
SqliteErrorCode.BUSY,
|
||||||
'8', # SQLITE_READONLY
|
SqliteErrorCode.READONLY,
|
||||||
'11', # SQLITE_CORRUPT
|
SqliteErrorCode.IOERR,
|
||||||
'13', # SQLITE_FULL
|
SqliteErrorCode.CORRUPT,
|
||||||
|
SqliteErrorCode.FULL,
|
||||||
|
SqliteErrorCode.CANTOPEN,
|
||||||
]
|
]
|
||||||
# At least in init(), we can get errors like this:
|
# At least in init(), we can get errors like this:
|
||||||
# > type: ConnectionError
|
# > type: ConnectionError
|
||||||
@ -82,38 +109,25 @@ class SqliteError(SqlError):
|
|||||||
"out of memory",
|
"out of memory",
|
||||||
]
|
]
|
||||||
errcode = error.nativeErrorCode()
|
errcode = error.nativeErrorCode()
|
||||||
self.environmental = (
|
if (errcode in environmental_errors or
|
||||||
errcode in environmental_errors or
|
(errcode == -1 and error.databaseText() in environmental_strings)):
|
||||||
(errcode == -1 and error.databaseText() in environmental_strings))
|
raise SqlEnvironmentError(msg, error)
|
||||||
|
else:
|
||||||
def text(self):
|
raise SqlBugError(msg, error)
|
||||||
return self.error.databaseText()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_query(cls, what, query, error):
|
|
||||||
"""Construct an error from a failed query.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
what: What we were doing when the error happened.
|
|
||||||
query: The query which was executed.
|
|
||||||
error: The QSqlError object.
|
|
||||||
"""
|
|
||||||
msg = 'Failed to {} query "{}": "{}"'.format(what, query, error.text())
|
|
||||||
return cls(msg, error)
|
|
||||||
|
|
||||||
|
|
||||||
def init(db_path):
|
def init(db_path):
|
||||||
"""Initialize the SQL database connection."""
|
"""Initialize the SQL database connection."""
|
||||||
database = QSqlDatabase.addDatabase('QSQLITE')
|
database = QSqlDatabase.addDatabase('QSQLITE')
|
||||||
if not database.isValid():
|
if not database.isValid():
|
||||||
raise SqlError('Failed to add database. '
|
raise SqlEnvironmentError('Failed to add database. Are sqlite and Qt '
|
||||||
'Are sqlite and Qt sqlite support installed?',
|
'sqlite support installed?')
|
||||||
environmental=True)
|
|
||||||
database.setDatabaseName(db_path)
|
database.setDatabaseName(db_path)
|
||||||
if not database.open():
|
if not database.open():
|
||||||
error = database.lastError()
|
error = database.lastError()
|
||||||
raise SqliteError("Failed to open sqlite database at {}: {}"
|
msg = "Failed to open sqlite database at {}: {}".format(db_path,
|
||||||
.format(db_path, error.text()), error)
|
error.text())
|
||||||
|
raise_sqlite_error(msg, error)
|
||||||
|
|
||||||
# Enable write-ahead-logging and reduce disk write frequency
|
# Enable write-ahead-logging and reduce disk write frequency
|
||||||
# see https://sqlite.org/pragma.html and issues #2930 and #3507
|
# see https://sqlite.org/pragma.html and issues #2930 and #3507
|
||||||
@ -135,11 +149,11 @@ def version():
|
|||||||
close()
|
close()
|
||||||
return ver
|
return ver
|
||||||
return Query("select sqlite_version()").run().value()
|
return Query("select sqlite_version()").run().value()
|
||||||
except SqlError as e:
|
except SqlEnvironmentError as e:
|
||||||
return 'UNAVAILABLE ({})'.format(e)
|
return 'UNAVAILABLE ({})'.format(e)
|
||||||
|
|
||||||
|
|
||||||
class Query(QSqlQuery):
|
class Query:
|
||||||
|
|
||||||
"""A prepared SQL Query."""
|
"""A prepared SQL Query."""
|
||||||
|
|
||||||
@ -151,39 +165,84 @@ class Query(QSqlQuery):
|
|||||||
forward_only: Optimization for queries that will only step forward.
|
forward_only: Optimization for queries that will only step forward.
|
||||||
Must be false for completion queries.
|
Must be false for completion queries.
|
||||||
"""
|
"""
|
||||||
super().__init__(QSqlDatabase.database())
|
self.query = QSqlQuery(QSqlDatabase.database())
|
||||||
|
|
||||||
log.sql.debug('Preparing SQL query: "{}"'.format(querystr))
|
log.sql.debug('Preparing SQL query: "{}"'.format(querystr))
|
||||||
if not self.prepare(querystr):
|
ok = self.query.prepare(querystr)
|
||||||
raise SqliteError.from_query('prepare', querystr, self.lastError())
|
self._check_ok('prepare', ok)
|
||||||
self.setForwardOnly(forward_only)
|
self.query.setForwardOnly(forward_only)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
if not self.isActive():
|
if not self.query.isActive():
|
||||||
raise SqlError("Cannot iterate inactive query")
|
raise SqlBugError("Cannot iterate inactive query")
|
||||||
rec = self.record()
|
rec = self.query.record()
|
||||||
fields = [rec.fieldName(i) for i in range(rec.count())]
|
fields = [rec.fieldName(i) for i in range(rec.count())]
|
||||||
rowtype = collections.namedtuple('ResultRow', fields)
|
rowtype = collections.namedtuple('ResultRow', fields)
|
||||||
|
|
||||||
while self.next():
|
while self.query.next():
|
||||||
rec = self.record()
|
rec = self.query.record()
|
||||||
yield rowtype(*[rec.value(i) for i in range(rec.count())])
|
yield rowtype(*[rec.value(i) for i in range(rec.count())])
|
||||||
|
|
||||||
|
def _check_ok(self, step, ok):
|
||||||
|
if not ok:
|
||||||
|
query = self.query.lastQuery()
|
||||||
|
error = self.query.lastError()
|
||||||
|
msg = 'Failed to {} query "{}": "{}"'.format(step, query,
|
||||||
|
error.text())
|
||||||
|
raise_sqlite_error(msg, error)
|
||||||
|
|
||||||
|
def _bind_values(self, values):
|
||||||
|
for key, val in values.items():
|
||||||
|
self.query.bindValue(':{}'.format(key), val)
|
||||||
|
if any(val is None for val in self.bound_values().values()):
|
||||||
|
raise SqlBugError("Missing bound values!")
|
||||||
|
|
||||||
def run(self, **values):
|
def run(self, **values):
|
||||||
"""Execute the prepared query."""
|
"""Execute the prepared query."""
|
||||||
log.sql.debug('Running SQL query: "{}"'.format(self.lastQuery()))
|
log.sql.debug('Running SQL query: "{}"'.format(
|
||||||
for key, val in values.items():
|
self.query.lastQuery()))
|
||||||
self.bindValue(':{}'.format(key), val)
|
|
||||||
log.sql.debug('query bindings: {}'.format(self.boundValues()))
|
self._bind_values(values)
|
||||||
if not self.exec_():
|
log.sql.debug('query bindings: {}'.format(self.bound_values()))
|
||||||
raise SqliteError.from_query('exec', self.lastQuery(),
|
|
||||||
self.lastError())
|
ok = self.query.exec_()
|
||||||
|
self._check_ok('exec', ok)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def run_batch(self, values):
|
||||||
|
"""Execute the query in batch mode."""
|
||||||
|
log.sql.debug('Running SQL query (batch): "{}"'.format(
|
||||||
|
self.query.lastQuery()))
|
||||||
|
|
||||||
|
self._bind_values(values)
|
||||||
|
|
||||||
|
db = QSqlDatabase.database()
|
||||||
|
ok = db.transaction()
|
||||||
|
self._check_ok('transaction', ok)
|
||||||
|
|
||||||
|
ok = self.query.execBatch()
|
||||||
|
try:
|
||||||
|
self._check_ok('execBatch', ok)
|
||||||
|
except SqlError:
|
||||||
|
# Not checking the return value here, as we're failing anyways...
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
ok = db.commit()
|
||||||
|
self._check_ok('commit', ok)
|
||||||
|
|
||||||
def value(self):
|
def value(self):
|
||||||
"""Return the result of a single-value query (e.g. an EXISTS)."""
|
"""Return the result of a single-value query (e.g. an EXISTS)."""
|
||||||
if not self.next():
|
if not self.query.next():
|
||||||
raise SqlError("No result for single-result query")
|
raise SqlBugError("No result for single-result query")
|
||||||
return self.record().value(0)
|
return self.query.record().value(0)
|
||||||
|
|
||||||
|
def rows_affected(self):
|
||||||
|
return self.query.numRowsAffected()
|
||||||
|
|
||||||
|
def bound_values(self):
|
||||||
|
return self.query.boundValues()
|
||||||
|
|
||||||
|
|
||||||
class SqlTable(QObject):
|
class SqlTable(QObject):
|
||||||
@ -266,7 +325,7 @@ class SqlTable(QObject):
|
|||||||
q = Query("DELETE FROM {table} where {field} = :val"
|
q = Query("DELETE FROM {table} where {field} = :val"
|
||||||
.format(table=self._name, field=field))
|
.format(table=self._name, field=field))
|
||||||
q.run(val=value)
|
q.run(val=value)
|
||||||
if not q.numRowsAffected():
|
if not q.rows_affected():
|
||||||
raise KeyError('No row with {} = "{}"'.format(field, value))
|
raise KeyError('No row with {} = "{}"'.format(field, value))
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
|
|
||||||
@ -296,14 +355,7 @@ class SqlTable(QObject):
|
|||||||
replace: If true, overwrite rows with a primary key match.
|
replace: If true, overwrite rows with a primary key match.
|
||||||
"""
|
"""
|
||||||
q = self._insert_query(values, replace)
|
q = self._insert_query(values, replace)
|
||||||
for key, val in values.items():
|
q.run_batch(values)
|
||||||
q.bindValue(':{}'.format(key), val)
|
|
||||||
|
|
||||||
db = QSqlDatabase.database()
|
|
||||||
db.transaction()
|
|
||||||
if not q.execBatch():
|
|
||||||
raise SqliteError.from_query('exec', q.lastQuery(), q.lastError())
|
|
||||||
db.commit()
|
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
|
|
||||||
def delete_all(self):
|
def delete_all(self):
|
||||||
|
@ -64,6 +64,7 @@ def whitelist_generator(): # noqa
|
|||||||
yield 'qutebrowser.utils.debug.qflags_key'
|
yield 'qutebrowser.utils.debug.qflags_key'
|
||||||
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
|
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
|
||||||
yield 'scripts.utils.bg_colors'
|
yield 'scripts.utils.bg_colors'
|
||||||
|
yield 'qutebrowser.misc.sql.SqliteErrorCode.CONSTRAINT'
|
||||||
|
|
||||||
# Qt attributes
|
# Qt attributes
|
||||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
|
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
|
||||||
|
@ -429,7 +429,7 @@ def test_config_change(config_stub, basedir, download_stub,
|
|||||||
|
|
||||||
host_blocker = adblock.HostBlocker()
|
host_blocker = adblock.HostBlocker()
|
||||||
host_blocker.read_hosts()
|
host_blocker.read_hosts()
|
||||||
config_stub.set_obj('content.host_blocking.lists', None)
|
config_stub.val.content.host_blocking.lists = None
|
||||||
host_blocker.read_hosts()
|
host_blocker.read_hosts()
|
||||||
for str_url in URLS_TO_CHECK:
|
for str_url in URLS_TO_CHECK:
|
||||||
assert not host_blocker.is_blocked(QUrl(str_url))
|
assert not host_blocker.is_blocked(QUrl(str_url))
|
||||||
|
@ -42,22 +42,16 @@ def hist(tmpdir):
|
|||||||
return history.WebHistory()
|
return history.WebHistory()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
class TestSpecialMethods:
|
||||||
def mock_time(mocker):
|
|
||||||
m = mocker.patch('qutebrowser.browser.history.time')
|
|
||||||
m.time.return_value = 12345
|
|
||||||
return 12345
|
|
||||||
|
|
||||||
|
def test_iter(self, hist):
|
||||||
def test_iter(hist):
|
|
||||||
urlstr = 'http://www.example.com/'
|
urlstr = 'http://www.example.com/'
|
||||||
url = QUrl(urlstr)
|
url = QUrl(urlstr)
|
||||||
hist.add_url(url, atime=12345)
|
hist.add_url(url, atime=12345)
|
||||||
|
|
||||||
assert list(hist) == [(urlstr, '', 12345, False)]
|
assert list(hist) == [(urlstr, '', 12345, False)]
|
||||||
|
|
||||||
|
def test_len(self, hist):
|
||||||
def test_len(hist):
|
|
||||||
assert len(hist) == 0
|
assert len(hist) == 0
|
||||||
|
|
||||||
url = QUrl('http://www.example.com/')
|
url = QUrl('http://www.example.com/')
|
||||||
@ -65,16 +59,18 @@ def test_len(hist):
|
|||||||
|
|
||||||
assert len(hist) == 1
|
assert len(hist) == 1
|
||||||
|
|
||||||
|
def test_contains(self, hist):
|
||||||
def test_contains(hist):
|
hist.add_url(QUrl('http://www.example.com/'), title='Title',
|
||||||
hist.add_url(QUrl('http://www.example.com/'), title='Title', atime=12345)
|
atime=12345)
|
||||||
assert 'http://www.example.com/' in hist
|
assert 'http://www.example.com/' in hist
|
||||||
assert 'www.example.com' not in hist
|
assert 'www.example.com' not in hist
|
||||||
assert 'Title' not in hist
|
assert 'Title' not in hist
|
||||||
assert 12345 not in hist
|
assert 12345 not in hist
|
||||||
|
|
||||||
|
|
||||||
def test_get_recent(hist):
|
class TestGetting:
|
||||||
|
|
||||||
|
def test_get_recent(self, hist):
|
||||||
hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890)
|
hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890)
|
||||||
hist.add_url(QUrl('http://example.com/'), atime=12345)
|
hist.add_url(QUrl('http://example.com/'), atime=12345)
|
||||||
assert list(hist.get_recent()) == [
|
assert list(hist.get_recent()) == [
|
||||||
@ -82,8 +78,7 @@ def test_get_recent(hist):
|
|||||||
('http://example.com/', '', 12345, False),
|
('http://example.com/', '', 12345, False),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_entries_between(self, hist):
|
||||||
def test_entries_between(hist):
|
|
||||||
hist.add_url(QUrl('http://www.example.com/1'), atime=12345)
|
hist.add_url(QUrl('http://www.example.com/1'), atime=12345)
|
||||||
hist.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
hist.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
||||||
hist.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
hist.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
||||||
@ -95,8 +90,7 @@ def test_entries_between(hist):
|
|||||||
times = [x.atime for x in hist.entries_between(12346, 12349)]
|
times = [x.atime for x in hist.entries_between(12346, 12349)]
|
||||||
assert times == [12349, 12348, 12348, 12347]
|
assert times == [12349, 12348, 12348, 12347]
|
||||||
|
|
||||||
|
def test_entries_before(self, hist):
|
||||||
def test_entries_before(hist):
|
|
||||||
hist.add_url(QUrl('http://www.example.com/1'), atime=12346)
|
hist.add_url(QUrl('http://www.example.com/1'), atime=12346)
|
||||||
hist.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
hist.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
||||||
hist.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
hist.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
||||||
@ -106,11 +100,14 @@ def test_entries_before(hist):
|
|||||||
hist.add_url(QUrl('http://www.example.com/7'), atime=12349)
|
hist.add_url(QUrl('http://www.example.com/7'), atime=12349)
|
||||||
hist.add_url(QUrl('http://www.example.com/8'), atime=12349)
|
hist.add_url(QUrl('http://www.example.com/8'), atime=12349)
|
||||||
|
|
||||||
times = [x.atime for x in hist.entries_before(12348, limit=3, offset=2)]
|
times = [x.atime for x in
|
||||||
|
hist.entries_before(12348, limit=3, offset=2)]
|
||||||
assert times == [12348, 12347, 12346]
|
assert times == [12348, 12347, 12346]
|
||||||
|
|
||||||
|
|
||||||
def test_clear(qtbot, tmpdir, hist, mocker):
|
class TestDelete:
|
||||||
|
|
||||||
|
def test_clear(self, qtbot, tmpdir, hist, mocker):
|
||||||
hist.add_url(QUrl('http://example.com/'))
|
hist.add_url(QUrl('http://example.com/'))
|
||||||
hist.add_url(QUrl('http://www.qutebrowser.org/'))
|
hist.add_url(QUrl('http://www.qutebrowser.org/'))
|
||||||
|
|
||||||
@ -119,20 +116,18 @@ def test_clear(qtbot, tmpdir, hist, mocker):
|
|||||||
hist.clear()
|
hist.clear()
|
||||||
assert m.called
|
assert m.called
|
||||||
|
|
||||||
|
def test_clear_force(self, qtbot, tmpdir, hist):
|
||||||
def test_clear_force(qtbot, tmpdir, hist):
|
|
||||||
hist.add_url(QUrl('http://example.com/'))
|
hist.add_url(QUrl('http://example.com/'))
|
||||||
hist.add_url(QUrl('http://www.qutebrowser.org/'))
|
hist.add_url(QUrl('http://www.qutebrowser.org/'))
|
||||||
hist.clear(force=True)
|
hist.clear(force=True)
|
||||||
assert not len(hist)
|
assert not len(hist)
|
||||||
assert not len(hist.completion)
|
assert not len(hist.completion)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('raw, escaped', [
|
||||||
@pytest.mark.parametrize('raw, escaped', [
|
|
||||||
('http://example.com/1', 'http://example.com/1'),
|
('http://example.com/1', 'http://example.com/1'),
|
||||||
('http://example.com/1 2', 'http://example.com/1%202'),
|
('http://example.com/1 2', 'http://example.com/1%202'),
|
||||||
])
|
])
|
||||||
def test_delete_url(hist, raw, escaped):
|
def test_delete_url(self, hist, raw, escaped):
|
||||||
hist.add_url(QUrl('http://example.com/'), atime=0)
|
hist.add_url(QUrl('http://example.com/'), atime=0)
|
||||||
hist.add_url(QUrl(escaped), atime=0)
|
hist.add_url(QUrl(escaped), atime=0)
|
||||||
hist.add_url(QUrl('http://example.com/2'), atime=0)
|
hist.add_url(QUrl('http://example.com/2'), atime=0)
|
||||||
@ -149,9 +144,16 @@ def test_delete_url(hist, raw, escaped):
|
|||||||
assert completion_diff == {(raw, '', 0)}
|
assert completion_diff == {(raw, '', 0)}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
class TestAdd:
|
||||||
'url, atime, title, redirect, history_url, completion_url', [
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def mock_time(self, mocker):
|
||||||
|
m = mocker.patch('qutebrowser.browser.history.time')
|
||||||
|
m.time.return_value = 12345
|
||||||
|
return 12345
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'url, atime, title, redirect, history_url, completion_url', [
|
||||||
('http://www.example.com', 12346, 'the title', False,
|
('http://www.example.com', 12346, 'the title', False,
|
||||||
'http://www.example.com', 'http://www.example.com'),
|
'http://www.example.com', 'http://www.example.com'),
|
||||||
('http://www.example.com', 12346, 'the title', True,
|
('http://www.example.com', 12346, 'the title', True,
|
||||||
@ -161,9 +163,9 @@ def test_delete_url(hist, raw, escaped):
|
|||||||
('https://user:pass@example.com', 12346, 'the title', False,
|
('https://user:pass@example.com', 12346, 'the title', False,
|
||||||
'https://user@example.com', 'https://user@example.com'),
|
'https://user@example.com', 'https://user@example.com'),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def test_add_url(qtbot, hist, url, atime, title, redirect, history_url,
|
def test_add_url(self, qtbot, hist,
|
||||||
completion_url):
|
url, atime, title, redirect, history_url, completion_url):
|
||||||
hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect)
|
hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect)
|
||||||
assert list(hist) == [(history_url, title, atime, redirect)]
|
assert list(hist) == [(history_url, title, atime, redirect)]
|
||||||
if completion_url is None:
|
if completion_url is None:
|
||||||
@ -171,20 +173,27 @@ def test_add_url(qtbot, hist, url, atime, title, redirect, history_url,
|
|||||||
else:
|
else:
|
||||||
assert list(hist.completion) == [(completion_url, title, atime)]
|
assert list(hist.completion) == [(completion_url, title, atime)]
|
||||||
|
|
||||||
|
def test_no_sql_history(self, hist, fake_args):
|
||||||
|
fake_args.debug_flags = 'no-sql-history'
|
||||||
|
hist.add_url(QUrl('https://www.example.com/'), atime=12346,
|
||||||
|
title='Hello World', redirect=False)
|
||||||
|
assert not list(hist)
|
||||||
|
|
||||||
def test_add_url_invalid(qtbot, hist, caplog):
|
def test_invalid(self, qtbot, hist, caplog):
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
hist.add_url(QUrl())
|
hist.add_url(QUrl())
|
||||||
assert not list(hist)
|
assert not list(hist)
|
||||||
assert not list(hist.completion)
|
assert not list(hist.completion)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('environmental', [True, False])
|
||||||
@pytest.mark.parametrize('environmental', [True, False])
|
@pytest.mark.parametrize('completion', [True, False])
|
||||||
@pytest.mark.parametrize('completion', [True, False])
|
def test_error(self, monkeypatch, hist, message_mock, caplog,
|
||||||
def test_add_url_error(monkeypatch, hist, message_mock, caplog,
|
|
||||||
environmental, completion):
|
environmental, completion):
|
||||||
def raise_error(url, replace=False):
|
def raise_error(url, replace=False):
|
||||||
raise sql.SqlError("Error message", environmental=environmental)
|
if environmental:
|
||||||
|
raise sql.SqlEnvironmentError("Error message")
|
||||||
|
else:
|
||||||
|
raise sql.SqlBugError("Error message")
|
||||||
|
|
||||||
if completion:
|
if completion:
|
||||||
monkeypatch.setattr(hist.completion, 'insert', raise_error)
|
monkeypatch.setattr(hist.completion, 'insert', raise_error)
|
||||||
@ -197,11 +206,10 @@ def test_add_url_error(monkeypatch, hist, message_mock, caplog,
|
|||||||
msg = message_mock.getmsg(usertypes.MessageLevel.error)
|
msg = message_mock.getmsg(usertypes.MessageLevel.error)
|
||||||
assert msg.text == "Failed to write history: Error message"
|
assert msg.text == "Failed to write history: Error message"
|
||||||
else:
|
else:
|
||||||
with pytest.raises(sql.SqlError):
|
with pytest.raises(sql.SqlBugError):
|
||||||
hist.add_url(QUrl('https://www.example.org/'))
|
hist.add_url(QUrl('https://www.example.org/'))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('level, url, req_url, expected', [
|
||||||
@pytest.mark.parametrize('level, url, req_url, expected', [
|
|
||||||
(logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]),
|
(logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]),
|
||||||
(logging.DEBUG, 'a.com', 'b.com', [('a.com', 'title', 12345, False),
|
(logging.DEBUG, 'a.com', 'b.com', [('a.com', 'title', 12345, False),
|
||||||
('b.com', 'title', 12345, True)]),
|
('b.com', 'title', 12345, True)]),
|
||||||
@ -209,15 +217,26 @@ def test_add_url_error(monkeypatch, hist, message_mock, caplog,
|
|||||||
(logging.WARNING, '', '', []),
|
(logging.WARNING, '', '', []),
|
||||||
(logging.WARNING, 'data:foo', '', []),
|
(logging.WARNING, 'data:foo', '', []),
|
||||||
(logging.WARNING, 'a.com', 'data:foo', []),
|
(logging.WARNING, 'a.com', 'data:foo', []),
|
||||||
])
|
])
|
||||||
def test_add_from_tab(hist, level, url, req_url, expected, mock_time, caplog):
|
def test_from_tab(self, hist, caplog, mock_time,
|
||||||
|
level, url, req_url, expected):
|
||||||
with caplog.at_level(level):
|
with caplog.at_level(level):
|
||||||
hist.add_from_tab(QUrl(url), QUrl(req_url), 'title')
|
hist.add_from_tab(QUrl(url), QUrl(req_url), 'title')
|
||||||
assert set(hist) == set(expected)
|
assert set(hist) == set(expected)
|
||||||
|
|
||||||
|
def test_exclude(self, hist, config_stub):
|
||||||
|
"""Excluded URLs should be in the history but not completion."""
|
||||||
|
config_stub.val.completion.web_history.exclude = ['*.example.org']
|
||||||
|
url = QUrl('http://www.example.org/')
|
||||||
|
hist.add_from_tab(url, url, 'title')
|
||||||
|
assert list(hist)
|
||||||
|
assert not list(hist.completion)
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def hist_interface(hist):
|
class TestHistoryInterface:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hist_interface(self, hist):
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
QtWebKit = pytest.importorskip('PyQt5.QtWebKit')
|
QtWebKit = pytest.importorskip('PyQt5.QtWebKit')
|
||||||
from qutebrowser.browser.webkit import webkithistory
|
from qutebrowser.browser.webkit import webkithistory
|
||||||
@ -229,16 +248,17 @@ def hist_interface(hist):
|
|||||||
yield
|
yield
|
||||||
QWebHistoryInterface.setDefaultInterface(None)
|
QWebHistoryInterface.setDefaultInterface(None)
|
||||||
|
|
||||||
|
def test_history_interface(self, qtbot, webview, hist_interface):
|
||||||
def test_history_interface(qtbot, webview, hist_interface):
|
|
||||||
html = b"<a href='about:blank'>foo</a>"
|
html = b"<a href='about:blank'>foo</a>"
|
||||||
url = urlutils.data_url('text/html', html)
|
url = urlutils.data_url('text/html', html)
|
||||||
with qtbot.waitSignal(webview.loadFinished):
|
with qtbot.waitSignal(webview.loadFinished):
|
||||||
webview.load(url)
|
webview.load(url)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
class TestInit:
|
||||||
def cleanup_init():
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cleanup_init(self):
|
||||||
# prevent test_init from leaking state
|
# prevent test_init from leaking state
|
||||||
yield
|
yield
|
||||||
hist = objreg.get('web-history', None)
|
hist = objreg.get('web-history', None)
|
||||||
@ -251,10 +271,9 @@ def cleanup_init():
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine,
|
||||||
@pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine,
|
|
||||||
usertypes.Backend.QtWebKit])
|
usertypes.Backend.QtWebKit])
|
||||||
def test_init(backend, qapp, tmpdir, monkeypatch, cleanup_init):
|
def test_init(self, backend, qapp, tmpdir, monkeypatch, cleanup_init):
|
||||||
if backend == usertypes.Backend.QtWebKit:
|
if backend == usertypes.Backend.QtWebKit:
|
||||||
pytest.importorskip('PyQt5.QtWebKitWidgets')
|
pytest.importorskip('PyQt5.QtWebKitWidgets')
|
||||||
else:
|
else:
|
||||||
@ -279,12 +298,15 @@ def test_init(backend, qapp, tmpdir, monkeypatch, cleanup_init):
|
|||||||
default_interface = None
|
default_interface = None
|
||||||
else:
|
else:
|
||||||
default_interface = QWebHistoryInterface.defaultInterface()
|
default_interface = QWebHistoryInterface.defaultInterface()
|
||||||
# For this to work, nothing can ever have called setDefaultInterface
|
# For this to work, nothing can ever have called
|
||||||
# before (so we need to test webengine before webkit)
|
# setDefaultInterface before (so we need to test webengine before
|
||||||
|
# webkit)
|
||||||
assert default_interface is None
|
assert default_interface is None
|
||||||
|
|
||||||
|
|
||||||
def test_import_txt(hist, data_tmpdir, monkeypatch, stubs):
|
class TestImport:
|
||||||
|
|
||||||
|
def test_import_txt(self, hist, data_tmpdir, monkeypatch, stubs):
|
||||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||||
histfile = data_tmpdir / 'history'
|
histfile = data_tmpdir / 'history'
|
||||||
# empty line is deliberate, to test skipping empty lines
|
# empty line is deliberate, to test skipping empty lines
|
||||||
@ -306,8 +328,7 @@ def test_import_txt(hist, data_tmpdir, monkeypatch, stubs):
|
|||||||
assert not histfile.exists()
|
assert not histfile.exists()
|
||||||
assert (data_tmpdir / 'history.bak').exists()
|
assert (data_tmpdir / 'history.bak').exists()
|
||||||
|
|
||||||
|
def test_existing_backup(self, hist, data_tmpdir, monkeypatch, stubs):
|
||||||
def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs):
|
|
||||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||||
histfile = data_tmpdir / 'history'
|
histfile = data_tmpdir / 'history'
|
||||||
bakfile = data_tmpdir / 'history.bak'
|
bakfile = data_tmpdir / 'history.bak'
|
||||||
@ -322,8 +343,7 @@ def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs):
|
|||||||
assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/',
|
assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/',
|
||||||
'12345 http://example.com/ title']
|
'12345 http://example.com/ title']
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('line', [
|
||||||
@pytest.mark.parametrize('line', [
|
|
||||||
'',
|
'',
|
||||||
'#12345 http://example.com/commented',
|
'#12345 http://example.com/commented',
|
||||||
|
|
||||||
@ -334,9 +354,10 @@ def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs):
|
|||||||
'12345 https://www..com/',
|
'12345 https://www..com/',
|
||||||
|
|
||||||
# issue #2646
|
# issue #2646
|
||||||
'12345 data:text/html;charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-',
|
('12345 data:text/html;'
|
||||||
])
|
'charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-'),
|
||||||
def test_import_txt_skip(hist, data_tmpdir, line, monkeypatch, stubs):
|
])
|
||||||
|
def test_skip(self, hist, data_tmpdir, monkeypatch, stubs, line):
|
||||||
"""import_txt should skip certain lines silently."""
|
"""import_txt should skip certain lines silently."""
|
||||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||||
histfile = data_tmpdir / 'history'
|
histfile = data_tmpdir / 'history'
|
||||||
@ -347,17 +368,16 @@ def test_import_txt_skip(hist, data_tmpdir, line, monkeypatch, stubs):
|
|||||||
assert not histfile.exists()
|
assert not histfile.exists()
|
||||||
assert not len(hist)
|
assert not len(hist)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('line', [
|
||||||
@pytest.mark.parametrize('line', [
|
|
||||||
'xyz http://example.com/bad-timestamp',
|
'xyz http://example.com/bad-timestamp',
|
||||||
'12345',
|
'12345',
|
||||||
'http://example.com/no-timestamp',
|
'http://example.com/no-timestamp',
|
||||||
'68891-r-r http://example.com/double-flag',
|
'68891-r-r http://example.com/double-flag',
|
||||||
'68891-x http://example.com/bad-flag',
|
'68891-x http://example.com/bad-flag',
|
||||||
'68891 http://.com',
|
'68891 http://.com',
|
||||||
])
|
])
|
||||||
def test_import_txt_invalid(hist, data_tmpdir, line, monkeypatch, stubs,
|
def test_invalid(self, hist, data_tmpdir, monkeypatch, stubs, caplog,
|
||||||
caplog):
|
line):
|
||||||
"""import_txt should fail on certain lines."""
|
"""import_txt should fail on certain lines."""
|
||||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||||
histfile = data_tmpdir / 'history'
|
histfile = data_tmpdir / 'history'
|
||||||
@ -371,14 +391,15 @@ def test_import_txt_invalid(hist, data_tmpdir, line, monkeypatch, stubs,
|
|||||||
|
|
||||||
assert histfile.exists()
|
assert histfile.exists()
|
||||||
|
|
||||||
|
def test_nonexistent(self, hist, data_tmpdir, monkeypatch, stubs):
|
||||||
def test_import_txt_nonexistent(hist, data_tmpdir, monkeypatch, stubs):
|
|
||||||
"""import_txt should do nothing if the history file doesn't exist."""
|
"""import_txt should do nothing if the history file doesn't exist."""
|
||||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||||
hist.import_txt()
|
hist.import_txt()
|
||||||
|
|
||||||
|
|
||||||
def test_debug_dump_history(hist, tmpdir):
|
class TestDump:
|
||||||
|
|
||||||
|
def test_debug_dump_history(self, hist, tmpdir):
|
||||||
hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345)
|
hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345)
|
||||||
hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346)
|
hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346)
|
||||||
hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347)
|
hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347)
|
||||||
@ -392,14 +413,15 @@ def test_debug_dump_history(hist, tmpdir):
|
|||||||
'12348-r http://example.com/4 Title4']
|
'12348-r http://example.com/4 Title4']
|
||||||
assert histfile.read() == '\n'.join(expected)
|
assert histfile.read() == '\n'.join(expected)
|
||||||
|
|
||||||
|
def test_nonexistent(self, hist, tmpdir):
|
||||||
def test_debug_dump_history_nonexistent(hist, tmpdir):
|
|
||||||
histfile = tmpdir / 'nonexistent' / 'history'
|
histfile = tmpdir / 'nonexistent' / 'history'
|
||||||
with pytest.raises(cmdexc.CommandError):
|
with pytest.raises(cmdexc.CommandError):
|
||||||
hist.debug_dump_history(str(histfile))
|
hist.debug_dump_history(str(histfile))
|
||||||
|
|
||||||
|
|
||||||
def test_rebuild_completion(hist):
|
class TestRebuild:
|
||||||
|
|
||||||
|
def test_delete(self, hist):
|
||||||
hist.insert({'url': 'example.com/1', 'title': 'example1',
|
hist.insert({'url': 'example.com/1', 'title': 'example1',
|
||||||
'redirect': False, 'atime': 1})
|
'redirect': False, 'atime': 1})
|
||||||
hist.insert({'url': 'example.com/1', 'title': 'example1',
|
hist.insert({'url': 'example.com/1', 'title': 'example1',
|
||||||
@ -418,9 +440,8 @@ def test_rebuild_completion(hist):
|
|||||||
('example.com/2 3', 'example2', 5),
|
('example.com/2 3', 'example2', 5),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_no_rebuild(self, hist):
|
||||||
def test_no_rebuild_completion(hist):
|
"""Ensure that completion is not regenerated unless empty."""
|
||||||
"""Ensure that completion is not regenerated unless completely empty."""
|
|
||||||
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||||
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||||
hist.completion.delete('url', 'example.com/2')
|
hist.completion.delete('url', 'example.com/2')
|
||||||
@ -428,9 +449,8 @@ def test_no_rebuild_completion(hist):
|
|||||||
hist2 = history.WebHistory()
|
hist2 = history.WebHistory()
|
||||||
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
||||||
|
|
||||||
|
def test_user_version(self, hist, monkeypatch):
|
||||||
def test_user_version(hist, monkeypatch):
|
"""Ensure that completion is regenerated if user_version changes."""
|
||||||
"""Ensure that completion is regenerated if user_version is incremented."""
|
|
||||||
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||||
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||||
hist.completion.delete('url', 'example.com/2')
|
hist.completion.delete('url', 'example.com/2')
|
||||||
@ -438,9 +458,72 @@ def test_user_version(hist, monkeypatch):
|
|||||||
hist2 = history.WebHistory()
|
hist2 = history.WebHistory()
|
||||||
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
||||||
|
|
||||||
monkeypatch.setattr(history, '_USER_VERSION', history._USER_VERSION + 1)
|
monkeypatch.setattr(history, '_USER_VERSION',
|
||||||
|
history._USER_VERSION + 1)
|
||||||
hist3 = history.WebHistory()
|
hist3 = history.WebHistory()
|
||||||
assert list(hist3.completion) == [
|
assert list(hist3.completion) == [
|
||||||
('example.com/1', '', 1),
|
('example.com/1', '', 1),
|
||||||
('example.com/2', '', 2),
|
('example.com/2', '', 2),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_force_rebuild(self, hist):
|
||||||
|
"""Ensure that completion is regenerated if we force a rebuild."""
|
||||||
|
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||||
|
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||||
|
hist.completion.delete('url', 'example.com/2')
|
||||||
|
|
||||||
|
hist2 = history.WebHistory()
|
||||||
|
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
||||||
|
hist2.metainfo['force_rebuild'] = True
|
||||||
|
|
||||||
|
hist3 = history.WebHistory()
|
||||||
|
assert list(hist3.completion) == [
|
||||||
|
('example.com/1', '', 1),
|
||||||
|
('example.com/2', '', 2),
|
||||||
|
]
|
||||||
|
assert not hist3.metainfo['force_rebuild']
|
||||||
|
|
||||||
|
def test_exclude(self, config_stub, hist):
|
||||||
|
"""Ensure that patterns in completion.web_history.exclude are ignored.
|
||||||
|
|
||||||
|
This setting should only be used for the completion.
|
||||||
|
"""
|
||||||
|
config_stub.val.completion.web_history.exclude = ['*.example.org']
|
||||||
|
assert hist.metainfo['force_rebuild']
|
||||||
|
|
||||||
|
hist.add_url(QUrl('http://example.com'), redirect=False, atime=1)
|
||||||
|
hist.add_url(QUrl('http://example.org'), redirect=False, atime=2)
|
||||||
|
|
||||||
|
hist2 = history.WebHistory()
|
||||||
|
assert list(hist2.completion) == [('http://example.com', '', 1)]
|
||||||
|
|
||||||
|
def test_unrelated_config_change(self, config_stub, hist):
|
||||||
|
config_stub.val.history_gap_interval = 1234
|
||||||
|
assert not hist.metainfo['force_rebuild']
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompletionMetaInfo:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def metainfo(self):
|
||||||
|
return history.CompletionMetaInfo()
|
||||||
|
|
||||||
|
def test_contains_keyerror(self, metainfo):
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
'does_not_exist' in metainfo # pylint: disable=pointless-statement
|
||||||
|
|
||||||
|
def test_getitem_keyerror(self, metainfo):
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
metainfo['does_not_exist'] # pylint: disable=pointless-statement
|
||||||
|
|
||||||
|
def test_setitem_keyerror(self, metainfo):
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
metainfo['does_not_exist'] = 42
|
||||||
|
|
||||||
|
def test_contains(self, metainfo):
|
||||||
|
assert 'force_rebuild' in metainfo
|
||||||
|
|
||||||
|
def test_modify(self, metainfo):
|
||||||
|
assert not metainfo['force_rebuild']
|
||||||
|
metainfo['force_rebuild'] = True
|
||||||
|
assert metainfo['force_rebuild']
|
||||||
|
@ -90,14 +90,15 @@ class TestHistoryHandler:
|
|||||||
for i in range(entry_count):
|
for i in range(entry_count):
|
||||||
entry_atime = now - i * interval
|
entry_atime = now - i * interval
|
||||||
entry = {"atime": str(entry_atime),
|
entry = {"atime": str(entry_atime),
|
||||||
"url": QUrl("www.x.com/" + str(i)),
|
"url": QUrl("http://www.x.com/" + str(i)),
|
||||||
"title": "Page " + str(i)}
|
"title": "Page " + str(i)}
|
||||||
items.insert(0, entry)
|
items.insert(0, entry)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_web_history(self, fake_save_manager, tmpdir, init_sql):
|
def fake_web_history(self, fake_save_manager, tmpdir, init_sql,
|
||||||
|
config_stub):
|
||||||
"""Create a fake web-history and register it into objreg."""
|
"""Create a fake web-history and register it into objreg."""
|
||||||
web_history = history.WebHistory()
|
web_history = history.WebHistory()
|
||||||
objreg.register('web-history', web_history)
|
objreg.register('web-history', web_history)
|
||||||
@ -133,6 +134,15 @@ class TestHistoryHandler:
|
|||||||
assert item['time'] <= start_time
|
assert item['time'] <= start_time
|
||||||
assert item['time'] > end_time
|
assert item['time'] > end_time
|
||||||
|
|
||||||
|
def test_exclude(self, fake_web_history, now, config_stub):
|
||||||
|
"""Make sure the completion.web_history.exclude setting is not used."""
|
||||||
|
config_stub.val.completion.web_history.exclude = ['www.x.com']
|
||||||
|
|
||||||
|
url = QUrl("qute://history/data?start_time={}".format(now))
|
||||||
|
_mimetype, data = qutescheme.qute_history(url)
|
||||||
|
items = json.loads(data)
|
||||||
|
assert items
|
||||||
|
|
||||||
def test_qute_history_benchmark(self, fake_web_history, benchmark, now):
|
def test_qute_history_benchmark(self, fake_web_history, benchmark, now):
|
||||||
r = range(100000)
|
r = range(100000)
|
||||||
entries = {
|
entries = {
|
||||||
|
@ -30,7 +30,7 @@ from qutebrowser.completion.models import histcategory
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def hist(init_sql, config_stub):
|
def hist(init_sql, config_stub):
|
||||||
config_stub.val.completion.timestamp_format = '%Y-%m-%d'
|
config_stub.val.completion.timestamp_format = '%Y-%m-%d'
|
||||||
config_stub.val.completion.web_history_max_items = -1
|
config_stub.val.completion.web_history.max_items = -1
|
||||||
return sql.SqlTable('CompletionHistory', ['url', 'title', 'last_atime'])
|
return sql.SqlTable('CompletionHistory', ['url', 'title', 'last_atime'])
|
||||||
|
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ def test_set_pattern_repeated(model_validator, hist):
|
|||||||
])
|
])
|
||||||
def test_sorting(max_items, before, after, model_validator, hist, config_stub):
|
def test_sorting(max_items, before, after, model_validator, hist, config_stub):
|
||||||
"""Validate the filtering and sorting results of set_pattern."""
|
"""Validate the filtering and sorting results of set_pattern."""
|
||||||
config_stub.val.completion.web_history_max_items = max_items
|
config_stub.val.completion.web_history.max_items = max_items
|
||||||
for url, title, atime in before:
|
for url, title, atime in before:
|
||||||
timestamp = datetime.datetime.strptime(atime, '%Y-%m-%d').timestamp()
|
timestamp = datetime.datetime.strptime(atime, '%Y-%m-%d').timestamp()
|
||||||
hist.insert({'url': url, 'title': title, 'last_atime': timestamp})
|
hist.insert({'url': url, 'title': title, 'last_atime': timestamp})
|
||||||
|
@ -172,7 +172,7 @@ def bookmarks(bookmark_manager_stub):
|
|||||||
def web_history(init_sql, stubs, config_stub):
|
def web_history(init_sql, stubs, config_stub):
|
||||||
"""Fixture which provides a web-history object."""
|
"""Fixture which provides a web-history object."""
|
||||||
config_stub.val.completion.timestamp_format = '%Y-%m-%d'
|
config_stub.val.completion.timestamp_format = '%Y-%m-%d'
|
||||||
config_stub.val.completion.web_history_max_items = -1
|
config_stub.val.completion.web_history.max_items = -1
|
||||||
stub = history.WebHistory()
|
stub = history.WebHistory()
|
||||||
objreg.register('web-history', stub)
|
objreg.register('web-history', stub)
|
||||||
yield stub
|
yield stub
|
||||||
@ -518,7 +518,7 @@ def test_url_completion_delete_history(qtmodeltester, info,
|
|||||||
def test_url_completion_zero_limit(config_stub, web_history, quickmarks, info,
|
def test_url_completion_zero_limit(config_stub, web_history, quickmarks, info,
|
||||||
bookmarks):
|
bookmarks):
|
||||||
"""Make sure there's no history if the limit was set to zero."""
|
"""Make sure there's no history if the limit was set to zero."""
|
||||||
config_stub.val.completion.web_history_max_items = 0
|
config_stub.val.completion.web_history.max_items = 0
|
||||||
model = urlmodel.url(info=info)
|
model = urlmodel.url(info=info)
|
||||||
model.set_pattern('')
|
model.set_pattern('')
|
||||||
category = model.index(2, 0) # "History" normally
|
category = model.index(2, 0) # "History" normally
|
||||||
|
@ -35,7 +35,7 @@ from PyQt5.QtGui import QColor, QFont
|
|||||||
from PyQt5.QtNetwork import QNetworkProxy
|
from PyQt5.QtNetwork import QNetworkProxy
|
||||||
|
|
||||||
from qutebrowser.config import configtypes, configexc
|
from qutebrowser.config import configtypes, configexc
|
||||||
from qutebrowser.utils import debug, utils, qtutils
|
from qutebrowser.utils import debug, utils, qtutils, urlmatch
|
||||||
from qutebrowser.browser.network import pac
|
from qutebrowser.browser.network import pac
|
||||||
from qutebrowser.keyinput import keyutils
|
from qutebrowser.keyinput import keyutils
|
||||||
from tests.helpers import utils as testutils
|
from tests.helpers import utils as testutils
|
||||||
@ -2099,6 +2099,21 @@ class TestKey:
|
|||||||
klass().to_py(val)
|
klass().to_py(val)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUrlPattern:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def klass(self):
|
||||||
|
return configtypes.UrlPattern
|
||||||
|
|
||||||
|
def test_to_py_valid(self, klass):
|
||||||
|
pattern = 'http://*.example.com/'
|
||||||
|
assert klass().to_py(pattern) == urlmatch.UrlPattern(pattern)
|
||||||
|
|
||||||
|
def test_to_py_invalid(self, klass):
|
||||||
|
with pytest.raises(configexc.ValidationError):
|
||||||
|
klass().to_py('http://')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('first, second, equal', [
|
@pytest.mark.parametrize('first, second, equal', [
|
||||||
(re.compile('foo'), RegexEq('foo'), True),
|
(re.compile('foo'), RegexEq('foo'), True),
|
||||||
(RegexEq('bar'), re.compile('bar'), True),
|
(RegexEq('bar'), re.compile('bar'), True),
|
||||||
|
@ -81,7 +81,7 @@ def test_changing_timer_with_messages_shown(qtbot, view, config_stub):
|
|||||||
config_stub.val.messages.timeout = 900000 # 15s
|
config_stub.val.messages.timeout = 900000 # 15s
|
||||||
view.show_message(usertypes.MessageLevel.info, 'test')
|
view.show_message(usertypes.MessageLevel.info, 'test')
|
||||||
with qtbot.waitSignal(view._clear_timer.timeout):
|
with qtbot.waitSignal(view._clear_timer.timeout):
|
||||||
config_stub.set_obj('messages.timeout', 100)
|
config_stub.val.messages.timeout = 100
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('count, expected', [(1, 100), (3, 300),
|
@pytest.mark.parametrize('count, expected', [(1, 100), (3, 300),
|
||||||
|
@ -29,30 +29,32 @@ from qutebrowser.misc import sql
|
|||||||
pytestmark = pytest.mark.usefixtures('init_sql')
|
pytestmark = pytest.mark.usefixtures('init_sql')
|
||||||
|
|
||||||
|
|
||||||
def test_sqlerror():
|
@pytest.mark.parametrize('klass', [sql.SqlEnvironmentError, sql.SqlBugError])
|
||||||
|
def test_sqlerror(klass):
|
||||||
text = "Hello World"
|
text = "Hello World"
|
||||||
err = sql.SqlError(text, environmental=True)
|
err = klass(text)
|
||||||
assert str(err) == text
|
assert str(err) == text
|
||||||
assert err.text() == text
|
assert err.text() == text
|
||||||
assert err.environmental
|
|
||||||
|
|
||||||
|
|
||||||
class TestSqliteError:
|
class TestSqlError:
|
||||||
|
|
||||||
@pytest.mark.parametrize('error_code, environmental', [
|
@pytest.mark.parametrize('error_code, exception', [
|
||||||
('5', True), # SQLITE_BUSY
|
(sql.SqliteErrorCode.BUSY, sql.SqlEnvironmentError),
|
||||||
('19', False), # SQLITE_CONSTRAINT
|
(sql.SqliteErrorCode.CONSTRAINT, sql.SqlBugError),
|
||||||
])
|
])
|
||||||
def test_environmental(self, error_code, environmental):
|
def test_environmental(self, error_code, exception):
|
||||||
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
||||||
error_code)
|
error_code)
|
||||||
err = sql.SqliteError("Message", sql_err)
|
with pytest.raises(exception):
|
||||||
assert err.environmental == environmental
|
sql.raise_sqlite_error("Message", sql_err)
|
||||||
|
|
||||||
def test_logging(self, caplog):
|
def test_logging(self, caplog):
|
||||||
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
||||||
'23')
|
'23')
|
||||||
sql.SqliteError("Message", sql_err)
|
with pytest.raises(sql.SqlBugError):
|
||||||
|
sql.raise_sqlite_error("Message", sql_err)
|
||||||
|
|
||||||
lines = [r.message for r in caplog.records]
|
lines = [r.message for r in caplog.records]
|
||||||
expected = ['SQL error:',
|
expected = ['SQL error:',
|
||||||
'type: UnknownError',
|
'type: UnknownError',
|
||||||
@ -62,21 +64,11 @@ class TestSqliteError:
|
|||||||
|
|
||||||
assert lines == expected
|
assert lines == expected
|
||||||
|
|
||||||
def test_from_query(self):
|
@pytest.mark.parametrize('klass',
|
||||||
|
[sql.SqlEnvironmentError, sql.SqlBugError])
|
||||||
|
def test_text(self, klass):
|
||||||
sql_err = QSqlError("driver text", "db text")
|
sql_err = QSqlError("driver text", "db text")
|
||||||
err = sql.SqliteError.from_query(
|
err = klass("Message", sql_err)
|
||||||
what='test', query='SELECT * from foo;', error=sql_err)
|
|
||||||
expected = ('Failed to test query "SELECT * from foo;": '
|
|
||||||
'"db text driver text"')
|
|
||||||
assert str(err) == expected
|
|
||||||
|
|
||||||
def test_subclass(self):
|
|
||||||
with pytest.raises(sql.SqlError):
|
|
||||||
raise sql.SqliteError("text", QSqlError())
|
|
||||||
|
|
||||||
def test_text(self):
|
|
||||||
sql_err = QSqlError("driver text", "db text")
|
|
||||||
err = sql.SqliteError("Message", sql_err)
|
|
||||||
assert err.text() == "db text"
|
assert err.text() == "db text"
|
||||||
|
|
||||||
|
|
||||||
@ -103,7 +95,7 @@ def test_insert_replace(qtbot):
|
|||||||
table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=True)
|
table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=True)
|
||||||
assert list(table) == [('one', 11, True)]
|
assert list(table) == [('one', 11, True)]
|
||||||
|
|
||||||
with pytest.raises(sql.SqlError):
|
with pytest.raises(sql.SqlBugError):
|
||||||
table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=False)
|
table.insert({'name': 'one', 'val': 11, 'lucky': True}, replace=False)
|
||||||
|
|
||||||
|
|
||||||
@ -139,7 +131,7 @@ def test_insert_batch_replace(qtbot):
|
|||||||
('one', 11, True),
|
('one', 11, True),
|
||||||
('nine', 19, True)]
|
('nine', 19, True)]
|
||||||
|
|
||||||
with pytest.raises(sql.SqlError):
|
with pytest.raises(sql.SqlBugError):
|
||||||
table.insert_batch({'name': ['one', 'nine'],
|
table.insert_batch({'name': ['one', 'nine'],
|
||||||
'val': [11, 19],
|
'val': [11, 19],
|
||||||
'lucky': [True, True]})
|
'lucky': [True, True]})
|
||||||
@ -231,3 +223,80 @@ def test_delete_all(qtbot):
|
|||||||
|
|
||||||
def test_version():
|
def test_version():
|
||||||
assert isinstance(sql.version(), str)
|
assert isinstance(sql.version(), str)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSqlQuery:
|
||||||
|
|
||||||
|
def test_prepare_error(self):
|
||||||
|
with pytest.raises(sql.SqlBugError) as excinfo:
|
||||||
|
sql.Query('invalid')
|
||||||
|
|
||||||
|
expected = ('Failed to prepare query "invalid": "near "invalid": '
|
||||||
|
'syntax error Unable to execute statement"')
|
||||||
|
assert str(excinfo.value) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('forward_only', [True, False])
|
||||||
|
def test_forward_only(self, forward_only):
|
||||||
|
q = sql.Query('SELECT 0 WHERE 0', forward_only=forward_only)
|
||||||
|
assert q.query.isForwardOnly() == forward_only
|
||||||
|
|
||||||
|
def test_iter_inactive(self):
|
||||||
|
q = sql.Query('SELECT 0')
|
||||||
|
with pytest.raises(sql.SqlBugError,
|
||||||
|
match='Cannot iterate inactive query'):
|
||||||
|
next(iter(q))
|
||||||
|
|
||||||
|
def test_iter_empty(self):
|
||||||
|
q = sql.Query('SELECT 0 AS col WHERE 0')
|
||||||
|
q.run()
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
next(iter(q))
|
||||||
|
|
||||||
|
def test_iter(self):
|
||||||
|
q = sql.Query('SELECT 0 AS col')
|
||||||
|
q.run()
|
||||||
|
result = next(iter(q))
|
||||||
|
assert result.col == 0
|
||||||
|
|
||||||
|
def test_iter_multiple(self):
|
||||||
|
q = sql.Query('VALUES (1), (2), (3);')
|
||||||
|
res = list(q.run())
|
||||||
|
assert len(res) == 3
|
||||||
|
assert res[0].column1 == 1
|
||||||
|
|
||||||
|
def test_run_binding(self):
|
||||||
|
q = sql.Query('SELECT :answer')
|
||||||
|
q.run(answer=42)
|
||||||
|
assert q.value() == 42
|
||||||
|
|
||||||
|
def test_run_missing_binding(self):
|
||||||
|
q = sql.Query('SELECT :answer')
|
||||||
|
with pytest.raises(sql.SqlBugError, match='Missing bound values!'):
|
||||||
|
q.run()
|
||||||
|
|
||||||
|
def test_run_batch(self):
|
||||||
|
q = sql.Query('SELECT :answer')
|
||||||
|
q.run_batch(values={'answer': [42]})
|
||||||
|
assert q.value() == 42
|
||||||
|
|
||||||
|
def test_run_batch_missing_binding(self):
|
||||||
|
q = sql.Query('SELECT :answer')
|
||||||
|
with pytest.raises(sql.SqlBugError, match='Missing bound values!'):
|
||||||
|
q.run_batch(values={})
|
||||||
|
|
||||||
|
def test_value_missing(self):
|
||||||
|
q = sql.Query('SELECT 0 WHERE 0')
|
||||||
|
q.run()
|
||||||
|
with pytest.raises(sql.SqlBugError,
|
||||||
|
match='No result for single-result query'):
|
||||||
|
q.value()
|
||||||
|
|
||||||
|
def test_num_rows_affected(self):
|
||||||
|
q = sql.Query('SELECT 0')
|
||||||
|
q.run()
|
||||||
|
assert q.rows_affected() == 0
|
||||||
|
|
||||||
|
def test_bound_values(self):
|
||||||
|
q = sql.Query('SELECT :answer')
|
||||||
|
q.run(answer=42)
|
||||||
|
assert q.bound_values() == {':answer': 42}
|
||||||
|
Loading…
Reference in New Issue
Block a user