mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-07-14 02:16:12 +02:00
Merge branch 'develop' into interactive_delete
This commit is contained in:
commit
972a00f478
58 changed files with 1910 additions and 840 deletions
29
features/contains.feature
Normal file
29
features/contains.feature
Normal 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"
|
|
@ -20,6 +20,7 @@ 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 ""
|
||||
|
|
|
@ -6,7 +6,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/bug153.dayone
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
template: false
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
|
|
12
features/data/configs/bug780.yaml
Normal file
12
features/data/configs/bug780.yaml
Normal 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: "|"
|
|
@ -7,7 +7,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/dayone.dayone
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/empty_folder
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ highlight: true
|
|||
journals:
|
||||
default: features/journals/encrypted.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -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: "|"
|
||||
|
|
|
@ -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: "|"
|
||||
|
|
|
@ -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: "|"
|
||||
|
|
|
@ -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: "|"
|
||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
|||
journals:
|
||||
default: features/journals/tags.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
||||
|
|
|
@ -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>
|
|
@ -1,7 +1,5 @@
|
|||
Feature: Dayone specific implementation details.
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Loading a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl -from 'feb 2013'"
|
||||
|
@ -15,7 +13,7 @@ Feature: Dayone specific implementation details.
|
|||
2013-07-17 11:38 This entry is starred!
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
# broken still
|
||||
@skip
|
||||
Scenario: Entries without timezone information will be interpreted as in the current timezone
|
||||
Given we use the config "dayone.yaml"
|
||||
|
@ -23,7 +21,6 @@ Feature: Dayone specific implementation details.
|
|||
Then we should get no error
|
||||
and the output should contain "2013-01-17T18:37Z" in the local time
|
||||
|
||||
@skip
|
||||
Scenario: Writing into Dayone
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl 01 may 1979: Being born hurts."
|
||||
|
@ -33,8 +30,6 @@ Feature: Dayone specific implementation details.
|
|||
1979-05-01 09:00 Being born hurts.
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Loading tags from a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl --tags"
|
||||
|
@ -44,8 +39,6 @@ Feature: Dayone specific implementation details.
|
|||
@play : 1
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Saving tags from a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl A hard day at @work"
|
||||
|
@ -56,8 +49,6 @@ Feature: Dayone specific implementation details.
|
|||
@play : 1
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Filtering by tags from a DayOne Journal
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl @work"
|
||||
|
@ -66,8 +57,6 @@ Feature: Dayone specific implementation details.
|
|||
2013-05-17 11:39 This entry has tags!
|
||||
"""
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
Scenario: Exporting dayone to json
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl --export json"
|
||||
|
|
|
@ -13,16 +13,16 @@ Feature: Zapped Dayone bugs stay dead!
|
|||
|
||||
# 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: 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"
|
||||
|
|
|
@ -3,29 +3,55 @@
|
|||
Given we use the config "encrypted.yaml"
|
||||
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
||||
Then the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
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" and "n"
|
||||
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 the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
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" and "y"
|
||||
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"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import shutil
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def before_feature(context, feature):
|
||||
|
@ -9,6 +10,10 @@ def before_feature(context, feature):
|
|||
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."""
|
||||
|
@ -36,6 +41,10 @@ def before_scenario(context, scenario):
|
|||
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."""
|
||||
|
|
|
@ -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!
|
||||
"""
|
||||
|
|
|
@ -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" and "y"
|
||||
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"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Feature: Zapped bugs should stay dead.
|
||||
|
||||
Scenario: Writing an entry does not print the entire journal
|
||||
# https://github.com/maebert/jrnl/issues/87
|
||||
# https://github.com/jrnl-org/jrnl/issues/87
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa."
|
||||
Then we should see the message "Entry added"
|
||||
|
@ -9,21 +9,14 @@ Feature: Zapped bugs should stay dead.
|
|||
Then the output should not contain "Life is good"
|
||||
|
||||
Scenario: Date with time should be parsed correctly
|
||||
# https://github.com/maebert/jrnl/issues/117
|
||||
# https://github.com/jrnl-org/jrnl/issues/117
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 2013-11-30 15:42: Project Started."
|
||||
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
|
||||
#https://github.com/jrnl-org/jrnl/issues/153
|
||||
Given we use the config "bug153.yaml"
|
||||
When we run "jrnl -1"
|
||||
Then we should get no error
|
||||
|
@ -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/jrnl-org/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."
|
||||
|
|
|
@ -3,10 +3,12 @@ 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
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
|
@ -17,7 +19,7 @@ import shlex
|
|||
import sys
|
||||
|
||||
consts = pdt.Constants(usePyICU=False)
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
|
@ -33,7 +35,7 @@ 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
|
||||
|
||||
|
||||
|
@ -44,23 +46,27 @@ keyring.set_keyring(TestKeyring())
|
|||
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)
|
||||
|
||||
|
||||
|
@ -70,14 +76,15 @@ 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)
|
||||
text = text or context.text
|
||||
|
||||
def _mock_editor_function(command):
|
||||
tmpfile = command[-1]
|
||||
with open(tmpfile, "w+") as f:
|
||||
|
@ -88,7 +95,7 @@ def open_editor_and_enter(context, text=""):
|
|||
|
||||
return tmpfile
|
||||
|
||||
with patch('subprocess.call', side_effect=_mock_editor_function):
|
||||
with patch("subprocess.call", side_effect=_mock_editor_function):
|
||||
run(context, "jrnl")
|
||||
|
||||
|
||||
|
@ -96,6 +103,7 @@ def _mock_getpass(inputs):
|
|||
def prompt_return(prompt="Password: "):
|
||||
print(prompt)
|
||||
return next(inputs)
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
|
@ -104,35 +112,44 @@ def _mock_input(inputs):
|
|||
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 "{inputs1}"')
|
||||
@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"')
|
||||
def run_with_input(context, command, inputs1="", inputs2=""):
|
||||
@when('we run "{command}" and enter "{inputs}"')
|
||||
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 inputs1:
|
||||
text = iter((inputs1, inputs2))
|
||||
elif context.text:
|
||||
if context.text:
|
||||
text = iter(context.text.split("\n"))
|
||||
else:
|
||||
text = iter(("", ""))
|
||||
text = iter([inputs])
|
||||
|
||||
args = ushlex(command)[1:]
|
||||
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input:
|
||||
with patch("jrnl.util.getpass", side_effect=_mock_getpass(text)) as mock_getpass:
|
||||
with 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
|
||||
|
||||
# assert at least one of the mocked input methods got called
|
||||
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||
# 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}"')
|
||||
|
@ -154,74 +171,32 @@ 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')
|
||||
|
@ -229,11 +204,11 @@ def check_output_time_inline(context, text):
|
|||
out = context.stdout_capture.getvalue()
|
||||
local_tz = tzlocal.get_localzone()
|
||||
date, flag = CALENDAR.parse(text)
|
||||
output_date = time.strftime("%Y-%m-%d %H:%M",date)
|
||||
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
|
||||
|
@ -270,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)
|
||||
|
||||
|
||||
|
@ -278,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]
|
||||
|
@ -290,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"):
|
||||
|
@ -299,6 +270,6 @@ def check_journal_entries(context, number, journal_name="default"):
|
|||
assert len(journal.entries) == number
|
||||
|
||||
|
||||
@then('fail')
|
||||
@then("fail")
|
||||
def debug_fail(context):
|
||||
assert False
|
||||
|
|
124
features/steps/export_steps.py
Normal file
124
features/steps/export_steps.py
Normal 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]
|
Loading…
Add table
Add a link
Reference in a new issue