Merge branch 'develop' into v2.5

This commit is contained in:
Jonathan Wren 2020-02-08 14:29:10 -08:00 committed by GitHub
commit 97cf65e516
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 2578 additions and 1204 deletions

29
features/contains.feature Normal file
View file

@ -0,0 +1,29 @@
Feature: Contains
Scenario: Searching for a string
Given we use the config "basic.yaml"
When we run "jrnl -contains life"
Then we should get no error
and the output should be
"""
2013-06-10 15:40 Life is good.
| But I'm better.
"""
Scenario: Searching for a string within tag results
Given we use the config "tags.yaml"
When we run "jrnl @idea -contains software"
Then we should get no error
and the output should contain "software"
Scenario: Searching for a string within AND tag results
Given we use the config "tags.yaml"
When we run "jrnl -and @journal @idea -contains software"
Then we should get no error
and the output should contain "software"
Scenario: Searching for a string within NOT tag results
Given we use the config "tags.yaml"
When we run "jrnl -not @dan -contains software"
Then we should get no error
and the output should contain "software"

View file

@ -20,6 +20,20 @@ Feature: Basic reading and writing to a journal
When we run "jrnl -n 1"
Then the output should contain "2013-07-23 09:00 A cold and stormy day."
@skip_win
Scenario: Writing an empty entry from the editor
Given we use the config "editor.yaml"
When we open the editor and enter ""
Then we should see the message "[Nothing saved to file]"
Scenario: Writing an empty entry from the command line
Given we use the config "basic.yaml"
When we run "jrnl" and enter ""
Then the output should be
"""
"""
Scenario: Filtering for dates
Given we use the config "basic.yaml"
When we run "jrnl -on 2013-06-10 --short"
@ -27,14 +41,6 @@ Feature: Basic reading and writing to a journal
When we run "jrnl -on 'june 6 2013' --short"
Then the output should be "2013-06-10 15:40 Life is good."
Scenario: Emoji support
Given we use the config "basic.yaml"
When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘"
Then we should see the message "Entry added"
When we run "jrnl -n 1"
Then the output should contain "🌞"
and the output should contain "🐘"
Scenario: Writing an entry at the prompt
Given we use the config "basic.yaml"
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."

View file

@ -0,0 +1,35 @@
Feature: Reading and writing to journal with custom date formats
Scenario: Loading a sample journal
Given we use the config "little_endian_dates.yaml"
When we run "jrnl -n 2"
Then we should get no error
And the output should be
"""
09.06.2013 15:39 My first entry.
| Everything is alright
10.06.2013 15:40 Life is good.
| But I'm better.
"""
Scenario: Writing an entry from command line
Given we use the config "little_endian_dates.yaml"
When we run "jrnl 2013-07-12: A cold and stormy day. I ate crisps on the sofa."
Then we should see the message "Entry added"
When we run "jrnl -n 1"
Then the output should contain "12.07.2013 09:00 A cold and stormy day."
Scenario: Filtering for dates
Given we use the config "little_endian_dates.yaml"
When we run "jrnl -on 2013-06-10 --short"
Then the output should be "10.06.2013 15:40 Life is good."
When we run "jrnl -on 'june 6 2013' --short"
Then the output should be "10.06.2013 15:40 Life is good."
Scenario: Writing an entry at the prompt
Given we use the config "little_endian_dates.yaml"
When we run "jrnl" and enter "2013-05-10: I saw Elvis. He's alive."
Then we should get no error
And the journal should contain "[10.05.2013 09:00] I saw Elvis."
And the journal should contain "He's alive."

View file

@ -6,7 +6,6 @@ highlight: true
journals:
default: features/journals/bug153.dayone
linewrap: 80
password: ''
tagsymbols: '@'
template: false
timeformat: '%Y-%m-%d %H:%M'

View file

@ -0,0 +1,12 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: false
highlight: true
journals:
default: features/journals/bug780.dayone
linewrap: 80
tagsymbols: '@'
template: false
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -7,7 +7,6 @@ highlight: true
journals:
default: features/journals/dayone.dayone
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -0,0 +1,12 @@
default_hour: 9
default_minute: 0
editor: "vim"
encrypt: false
highlight: true
journals:
default: features/journals/simple.journal
linewrap: 80
tagsymbols: "@"
template: false
timeformat: "%Y-%m-%d %H:%M"
indent_character: "|"

View file

@ -7,7 +7,6 @@ highlight: true
journals:
default: features/journals/empty_folder
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -7,7 +7,6 @@ highlight: true
journals:
default: features/journals/encrypted.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -0,0 +1,12 @@
default_hour: 9
default_minute: 0
editor: ""
encrypt: false
highlight: true
journals:
default: features/journals/little_endian_dates.journal
linewrap: 80
tagsymbols: "@"
template: false
timeformat: "%d.%m.%Y %H:%M"
indent_character: "|"

View file

@ -7,7 +7,6 @@ template: false
journals:
default: features/journals/markdown-headings-335.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -13,7 +13,6 @@ journals:
encrypt: true
journal: features/journals/new_encrypted.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -7,7 +7,6 @@ template: false
journals:
default: features/journals/tags-216.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -7,7 +7,6 @@ template: false
journals:
default: features/journals/tags-237.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -7,7 +7,6 @@ template: false
journals:
default: features/journals/tags.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -0,0 +1,11 @@
{
"default_hour": 9,
"timeformat": "%d.%m.%Y %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"journals": {"default": "features/journals/simple_jrnl-1-9-5_little_endian_dates.journal"},
"tagsymbols": "@"
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Activity</key>
<string>Stationary</string>
<key>Creation Date</key>
<date>2019-12-30T21:28:54Z</date>
<key>Entry Text</key>
<string></string>
<key>Starred</key>
<false />
<key>UUID</key>
<string>48A25033B34047C591160A4480197D8B</string>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>PC</string>
<key>Generation Date</key>
<date>2019-12-30T21:28:54Z</date>
<key>Host Name</key>
<string>LE-TREPORT</string>
<key>OS Agent</key>
<string>Microsoft Windows/10 Home</string>
<key>Software Agent</key>
<string>Journaley/2.1</string>
</dict>
<key>Tags</key>
<array>
<string>i_have_no_body</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,5 @@
[09.06.2013 15:39] My first entry.
Everything is alright
[10.06.2013 15:40] Life is good.
But I'm better.

View file

@ -0,0 +1,13 @@
10.06.2010 15:00 A life without chocolate is like a bad analogy.
10.06.2013 15:40 He said "[this] is the best time to be alive".
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
augue et venenatis facilisis.
[03.08.2019 12:55] Some chat log or something
Suspendisse potenti. Sed dignissim sed nisl eu consequat. Aenean ante ex,
elementum ut interdum et, mattis eget lacus. In commodo nulla nec tellus
placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at dolor
dui.

65
features/dayone.feature Normal file
View file

@ -0,0 +1,65 @@
Feature: Dayone specific implementation details.
Scenario: Loading a DayOne Journal
Given we use the config "dayone.yaml"
When we run "jrnl -from 'feb 2013'"
Then we should get no error
and the output should be
"""
2013-05-17 11:39 This entry has tags!
2013-06-17 20:38 This entry has a location.
2013-07-17 11:38 This entry is starred!
"""
# broken still
@skip
Scenario: Entries without timezone information will be interpreted as in the current timezone
Given we use the config "dayone.yaml"
When we run "jrnl -until 'feb 2013'"
Then we should get no error
and the output should contain "2013-01-17T18:37Z" in the local time
Scenario: Writing into Dayone
Given we use the config "dayone.yaml"
When we run "jrnl 01 may 1979: Being born hurts."
and we run "jrnl -until 1980"
Then the output should be
"""
1979-05-01 09:00 Being born hurts.
"""
Scenario: Loading tags from a DayOne Journal
Given we use the config "dayone.yaml"
When we run "jrnl --tags"
Then the output should be
"""
@work : 1
@play : 1
"""
Scenario: Saving tags from a DayOne Journal
Given we use the config "dayone.yaml"
When we run "jrnl A hard day at @work"
and we run "jrnl --tags"
Then the output should be
"""
@work : 2
@play : 1
"""
Scenario: Filtering by tags from a DayOne Journal
Given we use the config "dayone.yaml"
When we run "jrnl @work"
Then the output should be
"""
2013-05-17 11:39 This entry has tags!
"""
Scenario: Exporting dayone to json
Given we use the config "dayone.yaml"
When we run "jrnl --export json"
Then we should get no error
and the output should be parsable as json
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1"

View file

@ -0,0 +1,31 @@
Feature: Zapped Dayone bugs stay dead!
# fails when system time is UTC (as on Travis-CI)
@skip
Scenario: DayOne tag searching should work with tags containing a mixture of upper and lower case.
# https://github.com/jrnl-org/jrnl/issues/354
Given we use the config "dayone.yaml"
When we run "jrnl @plAy"
Then the output should contain
"""
2013-05-17 11:39 This entry has tags!
"""
# fails when system time is UTC (as on Travis-CI)
@skip
Scenario: Title with an embedded period on DayOne journal
Given we use the config "dayone.yaml"
When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch.""
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should be
"""
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
| I'm feeling sore because I forgot to stretch.
"""
Scenario: Opening an folder that's not a DayOne folder gives a nice error message
Given we use the config "empty_folder.yaml"
When we run "jrnl Herro"
Then we should get an error
Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either"

View file

@ -2,30 +2,56 @@
Scenario: Loading an encrypted journal
Given we use the config "encrypted.yaml"
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Then the output should contain "Password"
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Decrypting a journal
Given we use the config "encrypted.yaml"
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
Then the config for journal "default" should have "encrypt" set to "bool:False"
Then we should see the message "Journal decrypted"
and the journal should have 2 entries
And the journal should have 2 entries
Scenario: Encrypting a journal
Given we use the config "basic.yaml"
When we run "jrnl --encrypt" and enter "swordfish"
When we run "jrnl --encrypt" and enter
"""
swordfish
swordfish
n
"""
Then we should see the message "Journal encrypted"
and the config for journal "default" should have "encrypt" set to "bool:True"
And the config for journal "default" should have "encrypt" set to "bool:True"
When we run "jrnl -n 1" and enter "swordfish"
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Then the output should contain "Password"
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Mistyping your password
Given we use the config "basic.yaml"
When we run "jrnl --encrypt" and enter
"""
swordfish
sordfish
swordfish
swordfish
n
"""
Then we should see the message "Passwords did not match"
And we should see the message "Journal encrypted"
And the config for journal "default" should have "encrypt" set to "bool:True"
When we run "jrnl -n 1" and enter "swordfish"
Then the output should contain "Password"
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Storing a password in Keychain
Given we use the config "multiple.yaml"
When we run "jrnl simple --encrypt" and enter "sabertooth"
When we run "jrnl simple --encrypt" and enter
"""
sabertooth
sabertooth
y
"""
When we set the keychain password of "simple" to "sabertooth"
Then the config for journal "simple" should have "encrypt" set to "bool:True"
When we run "jrnl simple -n 1"
Then we should not see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Then the output should contain "2013-06-10 15:40 Life is good"

View file

@ -1,25 +1,28 @@
from behave import *
import shutil
import os
import jrnl
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
import sys
def before_feature(context, feature):
# add "skip" tag
# https://stackoverflow.com/a/42721605/4276230
if "skip" in feature.tags:
feature.skip("Marked with @skip")
return
if "skip_win" in feature.tags and "win32" in sys.platform:
feature.skip("Skipping on Windows")
return
def before_scenario(context, scenario):
"""Before each scenario, backup all config and journal test data."""
context.messages = StringIO()
jrnl.util.STDERR = context.messages
jrnl.util.TEST = True
# Clean up in case something went wrong
for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder)
if os.path.exists(working_dir):
shutil.rmtree(working_dir)
for folder in ("configs", "journals"):
original = os.path.join("features", "data", folder)
working_dir = os.path.join("features", folder)
@ -32,10 +35,19 @@ def before_scenario(context, scenario):
else:
shutil.copy2(source, working_dir)
# add "skip" tag
# https://stackoverflow.com/a/42721605/4276230
if "skip" in scenario.effective_tags:
scenario.skip("Marked with @skip")
return
if "skip_win" in scenario.effective_tags and "win32" in sys.platform:
scenario.skip("Skipping on Windows")
return
def after_scenario(context, scenario):
"""After each scenario, restore all test data and remove working_dirs."""
context.messages.close()
context.messages = None
for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder)
if os.path.exists(working_dir):

View file

@ -4,21 +4,20 @@ Feature: Exporting a Journal
Given we use the config "tags.yaml"
When we run "jrnl --export json"
Then we should get no error
and the output should be parsable as json
and "entries" in the json output should have 2 elements
and "tags" in the json output should contain "@idea"
and "tags" in the json output should contain "@journal"
and "tags" in the json output should contain "@dan"
And the output should be parsable as json
And "entries" in the json output should have 2 elements
And "tags" in the json output should contain "@idea"
And "tags" in the json output should contain "@journal"
And "tags" in the json output should contain "@dan"
Scenario: Exporting using filters should only export parts of the journal
Given we use the config "tags.yaml"
When we run "jrnl -until 'may 2013' --export json"
# Then we should get no error
Then the output should be parsable as json
and "entries" in the json output should have 1 element
and "tags" in the json output should contain "@idea"
and "tags" in the json output should contain "@journal"
and "tags" in the json output should not contain "@dan"
And "entries" in the json output should have 1 element
And "tags" in the json output should contain "@idea"
And "tags" in the json output should contain "@journal"
And "tags" in the json output should not contain "@dan"
Scenario: Exporting using custom templates
Given we use the config "basic.yaml"
@ -83,3 +82,57 @@ Feature: Exporting a Journal
More stuff
more stuff again
"""
Scenario: Exporting to XML
Given we use the config "tags.yaml"
When we run "jrnl --export xml"
Then the output should be a valid XML string
And "entries" node in the xml output should have 2 elements
And "tags" in the xml output should contain ["@idea", "@journal", "@dan"]
Scenario: Exporting tags
Given we use the config "tags.yaml"
When we run "jrnl --export tags"
Then the output should be
"""
@idea : 2
@journal : 1
@dan : 1
"""
Scenario: Exporting fancy
Given we use the config "tags.yaml"
When we run "jrnl --export fancy"
Then the output should be
"""
2013-04-09 15:39
I have an @idea:
(1) write a command line @journal software
(2) ???
(3) PROFIT!
2013-06-10 15:40
I met with @dan.
As alway's he shared his latest @idea on how to rule the world with me.
inst
"""
Scenario: Export to yaml
Given we use the config "tags.yaml"
And we created a directory named "exported_journal"
When we run "jrnl --export yaml -o exported_journal"
Then "exported_journal" should contain the files ["2013-04-09_i-have-an-idea.md", "2013-06-10_i-met-with-dan.md"]
And the content of exported yaml "exported_journal/2013-04-09_i-have-an-idea.md" should be
"""
title: I have an @idea:
date: 2013-04-09 15:39
stared: False
tags: idea, journal
(1) write a command line @journal software
(2) ???
(3) PROFIT!
"""

View file

@ -42,5 +42,10 @@ Feature: Multiple journals
Scenario: Don't crash if no file exists for a configured encrypted journal
Given we use the config "multiple.yaml"
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes"
Then we should see the message "Journal 'new_encrypted' created"
When we run "jrnl new_encrypted Adding first entry" and enter
"""
these three eyes
these three eyes
n
"""
Then we should see the message "Encrypted journal 'new_encrypted' created"

View file

@ -15,13 +15,6 @@ Feature: Zapped bugs should stay dead.
Then we should see the message "Entry added"
and the journal should contain "[2013-11-30 15:42] Project Started."
Scenario: Date in the future should be parsed correctly
# https://github.com/maebert/jrnl/issues/185
Given we use the config "basic.yaml"
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
Then we should see the message "Entry added"
and the journal should contain "[2019-06-26 09:00] Planet?"
Scenario: Loading entry with ambiguous time stamp
#https://github.com/maebert/jrnl/issues/153
Given we use the config "bug153.yaml"
@ -32,6 +25,19 @@ Feature: Zapped bugs should stay dead.
2013-10-27 03:27 Some text.
"""
Scenario: Date in the future should be parsed correctly
# https://github.com/maebert/jrnl/issues/185
Given we use the config "basic.yaml"
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
Then we should see the message "Entry added"
and the journal should contain "[2019-06-26 09:00] Planet?"
Scenario: Empty DayOne entry bodies should not error
# https://github.com/jrnl-org/jrnl/issues/780
Given we use the config "bug780.yaml"
When we run "jrnl --short"
Then we should get no error
Scenario: Title with an embedded period.
Given we use the config "basic.yaml"
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
@ -62,3 +68,57 @@ Feature: Zapped bugs should stay dead.
Then the output should contain "I'm going to activate the machine."
Then the output should contain "I've crossed so many timelines. Is there any going back?"
Scenario: Viewing today's entries does not print the entire journal
# https://github.com/jrnl-org/jrnl/issues/741
Given we use the config "basic.yaml"
When we run "jrnl -on today"
Then the output should not contain "Life is good"
Then the output should not contain "But I'm better."
Scenario: Create entry using day of the week as entry date.
Given we use the config "basic.yaml"
When we run "jrnl monday: This is an entry on a Monday."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should contain "monday at 9am" in the local time
Then the output should contain "This is an entry on a Monday."
Scenario: Create entry using day of the week abbreviations as entry date.
Given we use the config "basic.yaml"
When we run "jrnl fri: This is an entry on a Friday."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should contain "friday at 9am" in the local time
Scenario: Displaying entries using -on today should display entries created today.
Given we use the config "basic.yaml"
When we run "jrnl today: Adding an entry right now."
Then we should see the message "Entry added"
When we run "jrnl -on today"
Then the output should contain "Adding an entry right now."
Scenario: Displaying entries using -from day should display correct entries
Given we use the config "basic.yaml"
When we run "jrnl yesterday: This thing happened yesterday"
Then we should see the message "Entry added"
When we run "jrnl today at 11:59pm: Adding an entry right now."
Then we should see the message "Entry added"
When we run "jrnl tomorrow: A future entry."
Then we should see the message "Entry added"
When we run "jrnl -from today"
Then the output should contain "Adding an entry right now."
Then the output should contain "A future entry."
Then the output should not contain "This thing happened yesterday"
Scenario: Displaying entries using -from and -to day should display correct entries
Given we use the config "basic.yaml"
When we run "jrnl yesterday: This thing happened yesterday"
Then we should see the message "Entry added"
When we run "jrnl today at 11:59pm: Adding an entry right now."
Then we should see the message "Entry added"
When we run "jrnl tomorrow: A future entry."
Then we should see the message "Entry added"
When we run "jrnl -from yesterday -to today"
Then the output should contain "This thing happened yesterday"
Then the output should contain "Adding an entry right now."
Then the output should not contain "A future entry."

View file

@ -1,19 +1,30 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from unittest.mock import patch
from behave import given, when, then
from jrnl import cli, install, Journal, util, plugins
from jrnl import __version__
from dateutil import parser as date_parser
from collections import defaultdict
try:
import parsedatetime.parsedatetime_consts as pdt
except ImportError:
import parsedatetime as pdt
import time
import os
import json
import yaml
import keyring
import tzlocal
import shlex
import sys
consts = pdt.Constants(usePyICU=False)
consts.DOWParseStyle = -1 # Prefers past weekdays
CALENDAR = pdt.Calendar(consts)
class TestKeyring(keyring.backend.KeyringBackend):
"""A test keyring that just stores its valies in a hash"""
"""A test keyring that just stores its values in a hash"""
priority = 1
keys = defaultdict(dict)
@ -24,42 +35,38 @@ class TestKeyring(keyring.backend.KeyringBackend):
def get_password(self, servicename, username):
return self.keys[servicename].get(username)
def delete_password(self, servicename, username, password):
def delete_password(self, servicename, username):
self.keys[servicename][username] = None
# set the keyring for keyring lib
keyring.set_keyring(TestKeyring())
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
import tzlocal
import shlex
import sys
def ushlex(command):
if sys.version_info[0] == 3:
return shlex.split(command)
return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8')))
return map(lambda s: s.decode("UTF8"), shlex.split(command.encode("utf8")))
def read_journal(journal_name="default"):
config = util.load_config(install.CONFIG_FILE_PATH)
with open(config['journals'][journal_name]) as journal_file:
with open(config["journals"][journal_name]) as journal_file:
journal = journal_file.read()
return journal
def open_journal(journal_name="default"):
config = util.load_config(install.CONFIG_FILE_PATH)
journal_conf = config['journals'][journal_name]
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
journal_conf = config["journals"][journal_name]
if type(journal_conf) is dict:
# We can override the default config on a by-journal basis
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
else:
# But also just give them a string to point to the journal file
config["journal"] = journal_conf
return Journal.open_journal(journal_name, config)
@ -69,22 +76,80 @@ def set_config(context, config_file):
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
if config_file.endswith("yaml"):
# Add jrnl version to file for 2.x journals
with open(install.CONFIG_FILE_PATH, 'a') as cf:
with open(install.CONFIG_FILE_PATH, "a") as cf:
cf.write("version: {}".format(__version__))
@when('we open the editor and enter ""')
@when('we open the editor and enter "{text}"')
def open_editor_and_enter(context, text=""):
text = text or context.text
def _mock_editor_function(command):
tmpfile = command[-1]
with open(tmpfile, "w+") as f:
if text is not None:
f.write(text)
else:
f.write("")
return tmpfile
with patch("subprocess.call", side_effect=_mock_editor_function):
run(context, "jrnl")
def _mock_getpass(inputs):
def prompt_return(prompt="Password: "):
print(prompt)
return next(inputs)
return prompt_return
def _mock_input(inputs):
def prompt_return(prompt=""):
val = next(inputs)
print(prompt, val)
return val
return prompt_return
@when('we run "{command}" and enter')
@when('we run "{command}" and enter ""')
@when('we run "{command}" and enter "{inputs}"')
def run_with_input(context, command, inputs=None):
text = inputs or context.text
def run_with_input(context, command, inputs=""):
# create an iterator through all inputs. These inputs will be fed one by one
# to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()'
if context.text:
text = iter(context.text.split("\n"))
else:
text = iter([inputs])
args = ushlex(command)[1:]
buffer = StringIO(text.strip())
util.STDIN = buffer
try:
cli.run(args or [])
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
# fmt: off
# see: https://github.com/psf/black/issues/557
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \
patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass, \
patch("sys.stdin.read", side_effect=text) as mock_read:
try:
cli.run(args or [])
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
# at least one of the mocked input methods got called
assert mock_input.called or mock_getpass.called or mock_read.called
# all inputs were used
try:
next(text)
assert False, "Not all inputs were consumed"
except StopIteration:
pass
# fmt: on
@when('we run "{command}"')
@ -106,112 +171,66 @@ def load_template(context, filename):
@when('we set the keychain password of "{journal}" to "{password}"')
def set_keychain(context, journal, password):
keyring.set_password('jrnl', journal, password)
keyring.set_password("jrnl", journal, password)
@then('we should get an error')
@then("we should get an error")
def has_error(context):
assert context.exit_status != 0, context.exit_status
@then('we should get no error')
@then("we should get no error")
def no_error(context):
assert context.exit_status is 0, context.exit_status
assert context.exit_status == 0, context.exit_status
@then('the output should be parsable as json')
def check_output_json(context):
out = context.stdout_capture.getvalue()
assert json.loads(out), out
@then('"{field}" in the json output should have {number:d} elements')
@then('"{field}" in the json output should have 1 element')
def check_output_field(context, field, number=1):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json, [field, out_json]
assert len(out_json[field]) == number, len(out_json[field])
@then('"{field}" in the json output should not contain "{key}"')
def check_output_field_not_key(context, field, key):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json
assert key not in out_json[field]
@then('"{field}" in the json output should contain "{key}"')
def check_output_field_key(context, field, key):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json
assert key in out_json[field]
@then('the json output should contain {path} = "{value}"')
def check_json_output_path(context, path, value):
""" E.g.
the json output should contain entries.0.title = "hello"
"""
out = context.stdout_capture.getvalue()
struct = json.loads(out)
for node in path.split('.'):
try:
struct = struct[int(node)]
except ValueError:
struct = struct[node]
assert struct == value, struct
@then('the output should be')
@then("the output should be")
@then('the output should be "{text}"')
def check_output(context, text=None):
text = (text or context.text).strip().splitlines()
out = context.stdout_capture.getvalue().strip().splitlines()
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
assert len(text) == len(out), "Output has {} lines (expected: {})".format(
len(out), len(text)
)
for line_text, line_out in zip(text, out):
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
assert line_text.strip() == line_out.strip(), [
line_text.strip(),
line_out.strip(),
]
@then('the output should contain "{text}" in the local time')
def check_output_time_inline(context, text):
out = context.stdout_capture.getvalue()
local_tz = tzlocal.get_localzone()
utc_time = date_parser.parse(text)
local_date = utc_time.astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
assert local_date in out, local_date
date, flag = CALENDAR.parse(text)
output_date = time.strftime("%Y-%m-%d %H:%M", date)
assert output_date in out, output_date
@then('the output should contain')
@then("the output should contain")
@then('the output should contain "{text}"')
def check_output_inline(context, text=None):
text = text or context.text
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text in out, text
@then('the output should not contain "{text}"')
def check_output_not_inline(context, text):
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text not in out
@then('we should see the message "{text}"')
def check_message(context, text):
out = context.messages.getvalue()
out = context.stderr_capture.getvalue()
assert text in out, [text, out]
@then('we should not see the message "{text}"')
def check_not_message(context, text):
out = context.messages.getvalue()
out = context.stderr_capture.getvalue()
assert text not in out, [text, out]
@ -226,7 +245,7 @@ def check_journal_content(context, text, journal_name="default"):
def journal_doesnt_exist(context, journal_name="default"):
with open(install.CONFIG_FILE_PATH) as config_file:
config = yaml.load(config_file, Loader=yaml.FullLoader)
journal_path = config['journals'][journal_name]
journal_path = config["journals"][journal_name]
assert not os.path.exists(journal_path)
@ -234,11 +253,7 @@ def journal_doesnt_exist(context, journal_name="default"):
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
def config_var(context, key, value, journal=None):
t, value = value.split(":")
value = {
"bool": lambda v: v.lower() == "true",
"int": int,
"str": str
}[t](value)
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value)
config = util.load_config(install.CONFIG_FILE_PATH)
if journal:
config = config["journals"][journal]
@ -246,8 +261,8 @@ def config_var(context, key, value, journal=None):
assert config[key] == value
@then('the journal should have {number:d} entries')
@then('the journal should have {number:d} entry')
@then("the journal should have {number:d} entries")
@then("the journal should have {number:d} entry")
@then('journal "{journal_name}" should have {number:d} entries')
@then('journal "{journal_name}" should have {number:d} entry')
def check_journal_entries(context, number, journal_name="default"):
@ -264,6 +279,6 @@ def list_journal_directory(context, journal="default"):
for file in f:
print(os.path.join(root,file))
@then('fail')
@then("fail")
def debug_fail(context):
assert False

View file

@ -0,0 +1,124 @@
import json
import os
import shutil
from xml.etree import ElementTree
from behave import then, given
@then("the output should be parsable as json")
def check_output_json(context):
out = context.stdout_capture.getvalue()
assert json.loads(out), out
@then('"{field}" in the json output should have {number:d} elements')
@then('"{field}" in the json output should have 1 element')
def check_output_field(context, field, number=1):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json, [field, out_json]
assert len(out_json[field]) == number, len(out_json[field])
@then('"{field}" in the json output should not contain "{key}"')
def check_output_field_not_key(context, field, key):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json
assert key not in out_json[field]
@then('"{field}" in the json output should contain "{key}"')
def check_output_field_key(context, field, key):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json
assert key in out_json[field]
@then('the json output should contain {path} = "{value}"')
def check_json_output_path(context, path, value):
""" E.g.
the json output should contain entries.0.title = "hello"
"""
out = context.stdout_capture.getvalue()
struct = json.loads(out)
for node in path.split("."):
try:
struct = struct[int(node)]
except ValueError:
struct = struct[node]
assert struct == value, struct
@then("the output should be a valid XML string")
def assert_valid_xml_string(context):
output = context.stdout_capture.getvalue()
xml_tree = ElementTree.fromstring(output)
assert xml_tree, output
@then('"entries" node in the xml output should have {number:d} elements')
def assert_xml_output_entries_count(context, number):
output = context.stdout_capture.getvalue()
xml_tree = ElementTree.fromstring(output)
xml_tags = (node.tag for node in xml_tree)
assert "entries" in xml_tags, str(list(xml_tags))
actual_entry_count = len(xml_tree.find("entries"))
assert actual_entry_count == number, actual_entry_count
@then('"tags" in the xml output should contain {expected_tags_json_list}')
def assert_xml_output_tags(context, expected_tags_json_list):
output = context.stdout_capture.getvalue()
xml_tree = ElementTree.fromstring(output)
xml_tags = (node.tag for node in xml_tree)
assert "tags" in xml_tags, str(list(xml_tags))
expected_tags = json.loads(expected_tags_json_list)
actual_tags = set(t.attrib["name"] for t in xml_tree.find("tags"))
assert actual_tags == set(expected_tags), [actual_tags, set(expected_tags)]
@given('we created a directory named "{dir_name}"')
def create_directory(context, dir_name):
if os.path.exists(dir_name):
shutil.rmtree(dir_name)
os.mkdir(dir_name)
@then('"{dir_name}" should contain the files {expected_files_json_list}')
def assert_dir_contains_files(context, dir_name, expected_files_json_list):
actual_files = os.listdir(dir_name)
expected_files = json.loads(expected_files_json_list)
assert actual_files == expected_files, [actual_files, expected_files]
@then('the content of exported yaml "{file_path}" should be')
def assert_exported_yaml_file_content(context, file_path):
expected_content = context.text.strip().splitlines()
with open(file_path, "r") as f:
actual_content = f.read().strip().splitlines()
for actual_line, expected_line in zip(actual_content, expected_content):
if actual_line.startswith("tags: ") and expected_line.startswith("tags: "):
assert_equal_tags_ignoring_order(actual_line, expected_line)
else:
assert actual_line.strip() == expected_line.strip(), [
actual_line.strip(),
expected_line.strip(),
]
def assert_equal_tags_ignoring_order(actual_line, expected_line):
actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(","))
expected_tags = set(
tag.strip() for tag in expected_line[len("tags: ") :].split(",")
)
assert actual_tags == expected_tags, [actual_tags, expected_tags]

View file

@ -13,11 +13,22 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter
When we run "jrnl -n 1" and enter
"""
Y
bad doggie no biscuit
bad doggie no biscuit
"""
Then we should see the message "Password"
Then the output should contain "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Scenario: Upgrade and parse journals with little endian date format
Given we use the config "upgrade_from_195_little_endian_dates.json"
When we run "jrnl -9" and enter "Y"
Then the output should contain
"""
10.06.2010 15:00 A life without chocolate is like a bad analogy.
10.06.2013 15:40 He said "[this] is the best time to be alive".
"""
Then the journal should have 2 entries