diff --git a/README.rst b/README.rst index 416aafe..b1f8b41 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ format. An example config file with all available options:: [settings] debug = false api_key = your-api-key - hidefilenames = false + hide_filenames = false exclude = ^COMMIT_EDITMSG$ ^TAG_EDITMSG$ @@ -68,6 +68,7 @@ format. An example config file with all available options:: ^/etc/ include = .* + only_include_with_project_file = false offline = true proxy = https://user:pass@localhost:8080 no_ssl_verify = false diff --git a/tests/samples/configs/has_ssl_verify_disabled.cfg b/tests/samples/configs/has_ssl_verify_disabled.cfg index 65f8586..5ba2328 100644 --- a/tests/samples/configs/has_ssl_verify_disabled.cfg +++ b/tests/samples/configs/has_ssl_verify_disabled.cfg @@ -1,8 +1,8 @@ [settings] verbose = true api_key = d491a956-c8f2-44a9-98a7-987814bd71ba -logfile = /tmp/waka -hidefilenames = true +log_file = /tmp/waka +hide_filenames = true exclude = ^COMMIT_EDITMSG$ ^TAG_EDITMSG$ diff --git a/tests/samples/configs/hide_file_names.cfg b/tests/samples/configs/hide_file_names.cfg index 1b543c1..344f08c 100644 --- a/tests/samples/configs/hide_file_names.cfg +++ b/tests/samples/configs/hide_file_names.cfg @@ -1,6 +1,6 @@ [settings] debug = false api_key = 033c47c9-0441-4eb5-8b3f-b51f27b31049 -hidefilenames = +hide_filenames = missingfile python\.py$ diff --git a/tests/samples/configs/include_only_with_project_file.cfg b/tests/samples/configs/include_only_with_project_file.cfg new file mode 100644 index 0000000..e6b588e --- /dev/null +++ b/tests/samples/configs/include_only_with_project_file.cfg @@ -0,0 +1,3 @@ +[settings] +api_key = 1090a6ae-855f-4be7-b8fb-3edbaf1aa3ec +include_only_with_project_file = true diff --git a/tests/samples/configs/invalid_hide_file_names.cfg b/tests/samples/configs/invalid_hide_file_names.cfg index 7aec620..42fbe0a 100644 --- a/tests/samples/configs/invalid_hide_file_names.cfg +++ b/tests/samples/configs/invalid_hide_file_names.cfg @@ -1,4 +1,4 @@ [settings] debug = false api_key = 033c47c9-0441-4eb5-8b3f-b51f27b31049 -hidefilenames = invalid(regex +hide_filenames = invalid(regex diff --git a/tests/samples/configs/paranoid.cfg b/tests/samples/configs/paranoid.cfg index d71e751..4b490d6 100644 --- a/tests/samples/configs/paranoid.cfg +++ b/tests/samples/configs/paranoid.cfg @@ -12,4 +12,4 @@ include = \(invalid regex) includeme offline = true -hidefilenames = true +hide_filenames = true diff --git a/tests/samples/configs/paranoid_legacy.cfg b/tests/samples/configs/paranoid_legacy.cfg new file mode 100644 index 0000000..d71e751 --- /dev/null +++ b/tests/samples/configs/paranoid_legacy.cfg @@ -0,0 +1,15 @@ +[settings] +debug = false +api_key = c21f8ebd-6a6a-48a0-900b-0870db3d7afe +api_url = https://api.wakatime.com/api/v1/heartbeats +ignore = + COMMIT_EDITMSG$ + TAG_EDITMSG$ +exclude = + excludeme + \(invalid regex) +include = + \(invalid regex) + includeme +offline = true +hidefilenames = true diff --git a/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args b/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args index 843b9e3..b417a9a 100644 --- a/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args +++ b/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args @@ -2,8 +2,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] - [--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] - [--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] - [--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] + [--language LANGUAGE] [--hostname HOSTNAME] + [--disable-offline] [--hide-filenames] [--exclude EXCLUDE] + [--include INCLUDE] [--include-only-with-project-file] + [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] wakatime: error: Missing api key. Find your api key from wakatime.com/settings. diff --git a/tests/samples/output/configs_test_missing_config_file b/tests/samples/output/configs_test_missing_config_file index 843b9e3..b417a9a 100644 --- a/tests/samples/output/configs_test_missing_config_file +++ b/tests/samples/output/configs_test_missing_config_file @@ -2,8 +2,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] - [--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] - [--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] - [--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] + [--language LANGUAGE] [--hostname HOSTNAME] + [--disable-offline] [--hide-filenames] [--exclude EXCLUDE] + [--include INCLUDE] [--include-only-with-project-file] + [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] wakatime: error: Missing api key. Find your api key from wakatime.com/settings. diff --git a/tests/samples/output/main_test_timeout_passed_via_command_line b/tests/samples/output/main_test_timeout_passed_via_command_line index 67e012a..8f52543 100644 --- a/tests/samples/output/main_test_timeout_passed_via_command_line +++ b/tests/samples/output/main_test_timeout_passed_via_command_line @@ -2,8 +2,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] - [--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] - [--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] - [--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] + [--language LANGUAGE] [--hostname HOSTNAME] + [--disable-offline] [--hide-filenames] [--exclude EXCLUDE] + [--include INCLUDE] [--include-only-with-project-file] + [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] wakatime: error: argument --timeout: invalid int value: 'abc' diff --git a/tests/samples/output/test_help_contents b/tests/samples/output/test_help_contents index 59419ea..ff4a471 100644 --- a/tests/samples/output/test_help_contents +++ b/tests/samples/output/test_help_contents @@ -2,9 +2,10 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] - [--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] - [--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] - [--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] + [--language LANGUAGE] [--hostname HOSTNAME] + [--disable-offline] [--hide-filenames] [--exclude EXCLUDE] + [--include INCLUDE] [--include-only-with-project-file] + [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] Common interface for the WakaTime api. @@ -39,18 +40,21 @@ optional arguments: --language LANGUAGE optional language name; if valid, takes priority over auto-detected language --hostname HOSTNAME hostname of current machine. - --disableoffline disables offline time logging instead of queuing + --disable-offline disables offline time logging instead of queuing logged time - --hidefilenames obfuscate file names; will not send file names to api + --hide-filenames obfuscate filenames; will not send file names to api --exclude EXCLUDE filename patterns to exclude from logging; POSIX regex syntax; can be used more than once --include INCLUDE filename patterns to log; when used in combination with --exclude, files matching include will still be logged; POSIX regex syntax; can be used more than once + --include-only-with-project-file + disables tracking folders unless they contain a + .wakatime-project file; defaults to false --extra-heartbeats reads extra heartbeats from STDIN as a JSON array until EOF - --logfile LOGFILE defaults to ~/.wakatime.log - --apiurl API_URL heartbeats api url; for debugging with a local server + --log-file LOG_FILE defaults to ~/.wakatime.log + --api-url API_URL heartbeats api url; for debugging with a local server --timeout TIMEOUT number of seconds to wait when sending heartbeats to api; defaults to 60 seconds --config CONFIG defaults to ~/.wakatime.cfg diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 971703a..8c99ccd 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -12,13 +12,16 @@ import sys import uuid from testfixtures import log_capture from wakatime.arguments import parse_arguments -from wakatime.compat import u +from wakatime.compat import u, is_py3 from wakatime.constants import ( + API_ERROR, AUTH_ERROR, SUCCESS, ) +from wakatime.packages.requests.exceptions import RequestException +from wakatime.packages.requests.models import Response from wakatime.utils import get_user_agent -from .utils import mock, json, ANY, CustomResponse, TemporaryDirectory, TestCase +from .utils import mock, json, ANY, CustomResponse, TemporaryDirectory, TestCase, NamedTemporaryFile class ArgumentsTestCase(TestCase): @@ -944,12 +947,103 @@ class ArgumentsTestCase(TestCase): with mock.patch.object(sys, 'argv', ['wakatime'] + args): args, configs = parse_arguments() - self.assertEquals(args.logfile, None) + self.assertEquals(args.log_file, None) with mock.patch('os.environ.get') as mock_env: mock_env.return_value = os.path.realpath(tempdir) args, configs = parse_arguments() - self.assertEquals(args.logfile, logfile) + self.assertEquals(args.log_file, logfile) self.assertNothingPrinted() self.assertNothingLogged(logs) + + @log_capture() + def test_legacy_disableoffline_arg_supported(self, logs): + logging.disable(logging.NOTSET) + + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].side_effect = RequestException('requests exception') + + with TemporaryDirectory() as tempdir: + entity = 'tests/samples/codefiles/twolinefile.txt' + shutil.copy(entity, os.path.join(tempdir, 'twolinefile.txt')) + entity = os.path.realpath(os.path.join(tempdir, 'twolinefile.txt')) + now = u(int(time.time())) + key = str(uuid.uuid4()) + + args = ['--file', entity, '--key', key, '--disableoffline', + '--config', 'tests/samples/configs/good_config.cfg', '--time', now] + + retval = execute(args) + self.assertEquals(retval, API_ERROR) + self.assertNothingPrinted() + + log_output = u("\n").join([u(' ').join(x) for x in logs.actual()]) + expected = "WakaTime ERROR {'RequestException': u'requests exception'}" + if is_py3: + expected = "WakaTime ERROR {'RequestException': 'requests exception'}" + self.assertEquals(expected, log_output) + + self.assertHeartbeatSent() + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsNotSynced() + self.assertSessionCacheDeleted() + + def test_legacy_hidefilenames_arg_supported(self): + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse() + + with TemporaryDirectory() as tempdir: + entity = 'tests/samples/codefiles/python.py' + shutil.copy(entity, os.path.join(tempdir, 'python.py')) + entity = os.path.realpath(os.path.join(tempdir, 'python.py')) + now = u(int(time.time())) + config = 'tests/samples/configs/good_config.cfg' + key = str(uuid.uuid4()) + project = 'abcxyz' + + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--hidefilenames', '--logfile', '~/.wakatime.log', '--alternate-project', project] + + retval = execute(args) + self.assertEquals(retval, SUCCESS) + self.assertNothingPrinted() + + heartbeat = { + 'language': 'Python', + 'lines': None, + 'entity': 'HIDDEN.py', + 'project': project, + 'time': float(now), + 'is_write': False, + 'type': 'file', + 'dependencies': None, + 'user_agent': ANY, + } + self.assertHeartbeatSent(heartbeat) + + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsSynced() + self.assertSessionCacheSaved() + + @log_capture() + def test_deprecated_logfile_arg_supported(self, logs): + logging.disable(logging.NOTSET) + + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + with NamedTemporaryFile() as fh: + now = u(int(time.time())) + entity = 'tests/samples/codefiles/python.py' + config = 'tests/samples/configs/good_config.cfg' + logfile = os.path.realpath(fh.name) + args = ['--file', entity, '--config', config, '--time', now, '--logfile', logfile] + + execute(args) + + retval = execute(args) + self.assertEquals(retval, 102) + self.assertNothingPrinted() + + self.assertEquals(logging.WARNING, logging.getLogger('WakaTime').level) + self.assertEquals(logfile, logging.getLogger('WakaTime').handlers[0].baseFilename) + logs.check() diff --git a/tests/test_configs.py b/tests/test_configs.py index 7cf6709..2699cad 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -42,7 +42,7 @@ class ConfigsTestCase(TestCase): entity = 'tests/samples/codefiles/emptyfile.txt' shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) - args = ['--file', entity, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--log-file', '~/.wakatime.log'] with mock.patch('wakatime.configs.os.environ.get') as mock_env: mock_env.return_value = None @@ -77,7 +77,7 @@ class ConfigsTestCase(TestCase): with mock.patch('wakatime.configs.os.environ.get') as mock_env: mock_env.return_value = tempdir - args = ['--file', entity, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) expected_stdout = open('tests/samples/output/configs_test_good_config_file').read() @@ -100,7 +100,7 @@ class ConfigsTestCase(TestCase): shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) - args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log'] with self.assertRaises(SystemExit) as e: execute(args) @@ -122,7 +122,7 @@ class ConfigsTestCase(TestCase): entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) config = 'tests/samples/configs/has_everything.cfg' - args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) expected_stdout = open('tests/samples/output/configs_test_good_config_file').read() @@ -151,7 +151,7 @@ class ConfigsTestCase(TestCase): entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) config = 'tests/samples/configs/sample_alternate_apikey.cfg' - args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) self.assertEquals(sys.stdout.getvalue(), '') @@ -174,7 +174,7 @@ class ConfigsTestCase(TestCase): entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) config = 'tests/samples/configs/bad_config.cfg' - args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log'] with self.assertRaises(SystemExit) as e: execute(args) @@ -207,7 +207,7 @@ class ConfigsTestCase(TestCase): config = 'tests/samples/configs/good_config.cfg' key = str(uuid.uuid4()) - args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) @@ -246,7 +246,42 @@ class ConfigsTestCase(TestCase): key = u(uuid.uuid4()) project = 'abcxyz' - args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log', '--alternate-project', project] + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project] + + retval = execute(args) + self.assertEquals(retval, SUCCESS) + self.assertNothingPrinted() + + heartbeat = { + 'language': 'Python', + 'lines': None, + 'entity': 'HIDDEN.py', + 'project': project, + 'time': float(now), + 'is_write': False, + 'type': 'file', + 'dependencies': None, + 'user_agent': ANY, + } + self.assertHeartbeatSent(heartbeat) + + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsSynced() + self.assertSessionCacheSaved() + + def test_legacy_hidefilenames_config_supported(self): + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse() + + with TemporaryDirectory() as tempdir: + entity = 'tests/samples/codefiles/python.py' + shutil.copy(entity, os.path.join(tempdir, 'python.py')) + entity = os.path.realpath(os.path.join(tempdir, 'python.py')) + now = u(int(time.time())) + config = 'tests/samples/configs/paranoid_legacy.cfg' + key = u(uuid.uuid4()) + project = 'abcxyz' + + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project] retval = execute(args) self.assertEquals(retval, SUCCESS) @@ -281,7 +316,7 @@ class ConfigsTestCase(TestCase): key = str(uuid.uuid4()) project = 'abcxyz' - args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--hidefilenames', '--logfile', '~/.wakatime.log', '--alternate-project', project] + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--hide-filenames', '--log-file', '~/.wakatime.log', '--alternate-project', project] retval = execute(args) self.assertEquals(retval, SUCCESS) @@ -316,7 +351,7 @@ class ConfigsTestCase(TestCase): key = '033c47c9-0441-4eb5-8b3f-b51f27b31049' project = 'abcxyz' - args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log', '--alternate-project', project] + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project] retval = execute(args) self.assertEquals(retval, SUCCESS) @@ -355,7 +390,7 @@ class ConfigsTestCase(TestCase): dependencies = ['sqlalchemy', 'jinja', 'simplejson', 'flask', 'app', 'django', 'pygments', 'unittest', 'mock'] project = 'abcxyz' - args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log', '--alternate-project', project] + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project] retval = execute(args) self.assertEquals(retval, SUCCESS) @@ -392,7 +427,7 @@ class ConfigsTestCase(TestCase): config = 'tests/samples/configs/invalid_hide_file_names.cfg' key = str(uuid.uuid4()) - args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) @@ -438,7 +473,7 @@ class ConfigsTestCase(TestCase): entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) config = 'tests/samples/configs/good_config.cfg' - args = ['--file', entity, '--config', config, '--exclude', 'empty', '--verbose', '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--exclude', 'empty', '--verbose', '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) self.assertNothingPrinted() @@ -452,6 +487,77 @@ class ConfigsTestCase(TestCase): self.assertOfflineHeartbeatsSynced() self.assertSessionCacheUntouched() + @log_capture() + def test_exclude_file_without_project_file(self, logs): + logging.disable(logging.NOTSET) + + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + with TemporaryDirectory() as tempdir: + entity = 'tests/samples/codefiles/emptyfile.txt' + shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) + entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) + config = 'tests/samples/configs/include_only_with_project_file.cfg' + + args = ['--file', entity, '--config', config, '--verbose', '--log-file', '~/.wakatime.log'] + retval = execute(args) + self.assertEquals(retval, SUCCESS) + self.assertNothingPrinted() + actual = self.getLogOutput(logs) + expected = 'WakaTime DEBUG Skipping because missing .wakatime-project file in parent path.' + self.assertEquals(actual, expected) + + self.assertHeartbeatNotSent() + + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsSynced() + self.assertSessionCacheUntouched() + + @log_capture() + def test_include_file_with_project_file(self, logs): + logging.disable(logging.NOTSET) + + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse() + + with TemporaryDirectory() as tempdir: + entity = 'tests/samples/codefiles/emptyfile.txt' + shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) + entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) + config = 'tests/samples/configs/include_only_with_project_file.cfg' + project = 'abcxyz' + now = u(int(time.time())) + + with open(os.path.join(tempdir, '.wakatime-project'), 'w'): + pass + + args = ['--file', entity, '--config', config, '--time', now, '--verbose', '--log-file', '~/.wakatime.log', '--project', project] + retval = execute(args) + + self.assertEquals(retval, SUCCESS) + self.assertNothingPrinted() + + heartbeat = { + 'language': 'Text only', + 'lines': 0, + 'entity': os.path.realpath(entity), + 'project': project, + 'branch': ANY, + 'cursorpos': None, + 'lineno': None, + 'time': float(now), + 'is_write': False, + 'type': 'file', + 'dependencies': [], + 'user_agent': ANY, + } + self.assertHeartbeatSent(heartbeat) + + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsSynced() + self.assertSessionCacheSaved() + def test_hostname_set_from_config_file(self): self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse() @@ -462,7 +568,7 @@ class ConfigsTestCase(TestCase): hostname = 'fromcfgfile' config = 'tests/samples/configs/has_everything.cfg' - args = ['--file', entity, '--config', config, '--timeout', '15', '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--timeout', '15', '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) self.assertNothingPrinted() @@ -485,7 +591,7 @@ class ConfigsTestCase(TestCase): entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) config = 'tests/samples/configs/has_ssl_verify_disabled.cfg' - args = ['--file', entity, '--config', config, '--timeout', '15', '--logfile', '~/.wakatime.log'] + args = ['--file', entity, '--config', config, '--timeout', '15', '--log-file', '~/.wakatime.log'] retval = execute(args) self.assertEquals(retval, SUCCESS) self.assertNothingPrinted() diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py index d7b0628..75ef638 100644 --- a/tests/test_heartbeat.py +++ b/tests/test_heartbeat.py @@ -17,9 +17,10 @@ class HeartbeatTestCase(TestCase): class Args(object): exclude = [] - hidefilenames = ['.*'] + hide_filenames = ['.*'] include = [] plugin = None + include_only_with_project_file = None data = { 'entity': os.path.realpath('tests/samples/codefiles/python.py'), @@ -49,9 +50,10 @@ class HeartbeatTestCase(TestCase): class Args(object): exclude = [] - hidefilenames = [] + hide_filenames = [] include = [] plugin = None + include_only_with_project_file = None data = { 'entity': os.path.realpath('tests/samples/codefiles/python.py'), @@ -72,7 +74,7 @@ class HeartbeatTestCase(TestCase): logging.disable(logging.NOTSET) class Args(object): - hidefilenames = ['.*'] + hide_filenames = ['.*'] plugin = None branch = 'abc123' @@ -93,7 +95,7 @@ class HeartbeatTestCase(TestCase): logging.disable(logging.NOTSET) class Args(object): - hidefilenames = ['.*'] + hide_filenames = ['.*'] plugin = None branch = 'abc123' diff --git a/tests/test_logging.py b/tests/test_logging.py index 6d9f6cc..1758931 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -69,7 +69,7 @@ class LoggingTestCase(utils.TestCase): entity = 'tests/samples/codefiles/python.py' config = 'tests/samples/configs/good_config.cfg' logfile = os.path.realpath(fh.name) - args = ['--file', entity, '--config', config, '--time', now, '--logfile', logfile] + args = ['--file', entity, '--config', config, '--time', now, '--log-file', logfile] execute(args) diff --git a/tests/test_main.py b/tests/test_main.py index 090db11..fcd2d6a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -163,7 +163,7 @@ class MainTestCase(utils.TestCase): 'dependencies': [], } - args = ['--file', entity, '--key', key, '--disableoffline', + args = ['--file', entity, '--key', key, '--disable-offline', '--config', 'tests/samples/configs/good_config.cfg', '--time', now] retval = execute(args) @@ -244,7 +244,7 @@ class MainTestCase(utils.TestCase): now = u(int(time.time())) key = str(uuid.uuid4()) - args = ['--file', entity, '--key', key, '--disableoffline', + args = ['--file', entity, '--key', key, '--disable-offline', '--config', 'tests/samples/configs/good_config.cfg', '--time', now] retval = execute(args) diff --git a/tests/test_session_cache.py b/tests/test_session_cache.py index f6a2549..cd7001e 100644 --- a/tests/test_session_cache.py +++ b/tests/test_session_cache.py @@ -25,7 +25,7 @@ class SessionCacheTestCase(utils.TestCase): version = '' plugin = '' verbose = False - logfile = '' + log_file = '' self.args = MockArgs() setup_logging(self.args, self.args.version) diff --git a/wakatime/arguments.py b/wakatime/arguments.py index 81e24fe..a2ff819 100644 --- a/wakatime/arguments.py +++ b/wakatime/arguments.py @@ -103,12 +103,17 @@ def parse_arguments(): 'auto-detected language') parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+ 'current machine.') - parser.add_argument('--disableoffline', dest='offline', + parser.add_argument('--disable-offline', dest='offline', action='store_false', help='disables offline time logging instead of queuing logged time') + parser.add_argument('--disableoffline', dest='offline_deprecated', + action='store_true', help=argparse.SUPPRESS) + parser.add_argument('--hide-filenames', dest='hide_filenames', + action='store_true', + help='obfuscate filenames; will not send file names to api') parser.add_argument('--hidefilenames', dest='hidefilenames', action='store_true', - help='obfuscate file names; will not send file names to api') + help=argparse.SUPPRESS) parser.add_argument('--exclude', dest='exclude', action='append', help='filename patterns to exclude from logging; POSIX regex '+ 'syntax; can be used more than once') @@ -116,15 +121,24 @@ def parse_arguments(): help='filename patterns to log; when used in combination with '+ '--exclude, files matching include will still be logged; '+ 'POSIX regex syntax; can be used more than once') + parser.add_argument('--include-only-with-project-file', + dest='include_only_with_project_file', + action='store_true', + help='disables tracking folders unless they contain '+ + 'a .wakatime-project file; defaults to false') parser.add_argument('--ignore', dest='ignore', action='append', help=argparse.SUPPRESS) parser.add_argument('--extra-heartbeats', dest='extra_heartbeats', action='store_true', help='reads extra heartbeats from STDIN as a JSON array until EOF') - parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes, + parser.add_argument('--log-file', dest='log_file', action=StoreWithoutQuotes, help='defaults to ~/.wakatime.log') - parser.add_argument('--apiurl', dest='api_url', action=StoreWithoutQuotes, + parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes, + help=argparse.SUPPRESS) + parser.add_argument('--api-url', dest='api_url', action=StoreWithoutQuotes, help='heartbeats api url; for debugging with a local server') + parser.add_argument('--apiurl', dest='apiurl', action=StoreWithoutQuotes, + help=argparse.SUPPRESS) parser.add_argument('--timeout', dest='timeout', type=int, action=StoreWithoutQuotes, help='number of seconds to wait when sending heartbeats to api; '+ 'defaults to 60 seconds') @@ -194,6 +208,8 @@ def parse_arguments(): args.exclude.append(pattern) except TypeError: # pragma: nocover pass + if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'): + args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file') if not args.include: args.include = [] if configs.has_option('settings', 'include'): @@ -203,18 +219,26 @@ def parse_arguments(): args.include.append(pattern) except TypeError: # pragma: nocover pass - if args.hidefilenames: - args.hidefilenames = ['.*'] + if not args.hide_filenames and args.hidefilenames: + args.hide_filenames = args.hidefilenames + if args.hide_filenames: + args.hide_filenames = ['.*'] else: - args.hidefilenames = [] + args.hide_filenames = [] + option = None if configs.has_option('settings', 'hidefilenames'): option = configs.get('settings', 'hidefilenames') + if configs.has_option('settings', 'hide_filenames'): + option = configs.get('settings', 'hide_filenames') + if option is not None: if option.strip().lower() == 'true': - args.hidefilenames = ['.*'] + args.hide_filenames = ['.*'] elif option.strip().lower() != 'false': for pattern in option.split("\n"): if pattern.strip() != '': - args.hidefilenames.append(pattern) + args.hide_filenames.append(pattern) + if args.offline_deprecated: + args.offline = False if args.offline and configs.has_option('settings', 'offline'): args.offline = configs.getboolean('settings', 'offline') if not args.proxy and configs.has_option('settings', 'proxy'): @@ -235,11 +259,15 @@ def parse_arguments(): args.verbose = configs.getboolean('settings', 'verbose') if not args.verbose and configs.has_option('settings', 'debug'): args.verbose = configs.getboolean('settings', 'debug') - if not args.logfile and configs.has_option('settings', 'logfile'): - args.logfile = configs.get('settings', 'logfile') - if not args.logfile and os.environ.get('WAKATIME_HOME'): + if not args.log_file and args.logfile: + args.log_file = args.logfile + if not args.log_file and configs.has_option('settings', 'log_file'): + args.log_file = configs.get('settings', 'log_file') + if not args.log_file and os.environ.get('WAKATIME_HOME'): home = os.environ.get('WAKATIME_HOME') - args.logfile = os.path.join(os.path.expanduser(home), '.wakatime.log') + args.log_file = os.path.join(os.path.expanduser(home), '.wakatime.log') + if not args.api_url and args.apiurl: + args.api_url = args.apiurl if not args.api_url and configs.has_option('settings', 'api_url'): args.api_url = configs.get('settings', 'api_url') if not args.timeout and configs.has_option('settings', 'timeout'): diff --git a/wakatime/heartbeat.py b/wakatime/heartbeat.py index 6b93632..c0b2e02 100644 --- a/wakatime/heartbeat.py +++ b/wakatime/heartbeat.py @@ -14,7 +14,7 @@ import re from .compat import u, json from .project import get_project_info from .stats import get_file_stats -from .utils import get_user_agent, should_exclude, format_file_path +from .utils import get_user_agent, should_exclude, format_file_path, find_project_file log = logging.getLogger('WakaTime') @@ -66,9 +66,12 @@ class Heartbeat(object): return if self.type == 'file': self.entity = format_file_path(self.entity) - if self.type == 'file' and (not self.entity or not os.path.isfile(self.entity)): - self.skip = u('File does not exist; ignoring this heartbeat.') - return + if not self.entity or not os.path.isfile(self.entity): + self.skip = u('File does not exist; ignoring this heartbeat.') + return + if self._excluded_by_missing_project_file(): + self.skip = u('Skipping because missing .wakatime-project file in parent path.') + return project, branch = get_project_info(configs, self, data) self.project = project @@ -103,7 +106,7 @@ class Heartbeat(object): Returns a Heartbeat. """ - if not self.args.hidefilenames: + if not self.args.hide_filenames: return self if self.entity is None: @@ -112,7 +115,7 @@ class Heartbeat(object): if self.type != 'file': return self - for pattern in self.args.hidefilenames: + for pattern in self.args.hide_filenames: try: compiled = re.compile(pattern, re.IGNORECASE) if compiled.search(self.entity): @@ -183,6 +186,11 @@ class Heartbeat(object): def _excluded_by_pattern(self): return should_exclude(self.entity, self.args.include, self.args.exclude) + def _excluded_by_missing_project_file(self): + if not self.args.include_only_with_project_file: + return False + return find_project_file(self.entity) is None + def __repr__(self): return self.json() diff --git a/wakatime/logger.py b/wakatime/logger.py index 2883ad7..9bb51fe 100644 --- a/wakatime/logger.py +++ b/wakatime/logger.py @@ -75,7 +75,7 @@ def setup_logging(args, version): for handler in logger.handlers: logger.removeHandler(handler) set_log_level(logger, args) - logfile = args.logfile + logfile = args.log_file if not logfile: logfile = '~/.wakatime.log' handler = logging.FileHandler(os.path.expanduser(logfile)) diff --git a/wakatime/projects/projectfile.py b/wakatime/projects/projectfile.py index 9ca6d80..73d5cc3 100644 --- a/wakatime/projects/projectfile.py +++ b/wakatime/projects/projectfile.py @@ -12,11 +12,11 @@ """ import logging -import os import sys from .base import BaseProject from ..compat import u, open +from ..utils import find_project_file log = logging.getLogger('WakaTime') @@ -25,7 +25,7 @@ log = logging.getLogger('WakaTime') class ProjectFile(BaseProject): def process(self): - self.config = self._find_config(self.path) + self.config = find_project_file(self.path) self._project_name = None self._project_branch = None @@ -33,13 +33,13 @@ class ProjectFile(BaseProject): try: with open(self.config, 'r', encoding='utf-8') as fh: - self._project_name = u(fh.readline().strip()) - self._project_branch = u(fh.readline().strip()) + self._project_name = u(fh.readline().strip()) or None + self._project_branch = u(fh.readline().strip()) or None except UnicodeDecodeError: # pragma: nocover try: with open(self.config, 'r', encoding=sys.getfilesystemencoding()) as fh: - self._project_name = u(fh.readline().strip()) - self._project_branch = u(fh.readline().strip()) + self._project_name = u(fh.readline().strip()) or None + self._project_branch = u(fh.readline().strip()) or None except: log.traceback(logging.WARNING) except IOError: # pragma: nocover @@ -53,14 +53,3 @@ class ProjectFile(BaseProject): def branch(self): return self._project_branch - - def _find_config(self, path): - path = os.path.realpath(path) - if os.path.isfile(path): - path = os.path.split(path)[0] - if os.path.isfile(os.path.join(path, '.wakatime-project')): - return os.path.join(path, '.wakatime-project') - split_path = os.path.split(path) - if split_path[1] == '': - return None - return self._find_config(split_path[0]) diff --git a/wakatime/utils.py b/wakatime/utils.py index f85ab1f..d7a3bc6 100644 --- a/wakatime/utils.py +++ b/wakatime/utils.py @@ -82,3 +82,15 @@ def format_file_path(filepath): def get_hostname(args): return args.hostname or socket.gethostname() + + +def find_project_file(path): + path = os.path.realpath(path) + if os.path.isfile(path): + path = os.path.split(path)[0] + if os.path.isfile(os.path.join(path, '.wakatime-project')): + return os.path.join(path, '.wakatime-project') + split_path = os.path.split(path) + if split_path[1] == '': + return None + return find_project_file(split_path[0])