diff --git a/tests/test_languages.py b/tests/test_languages.py index 13a55ba..76b68c0 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -157,3 +157,44 @@ class LanguagesTestCase(utils.TestCase): language = u('Java') self.assertEqual(self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0].get('language'), language) + + def test_alternate_language_not_used_when_invalid(self): + response = Response() + response.status_code = 500 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + with utils.mock.patch('wakatime.stats.smart_guess_lexer') as mock_guess_lexer: + mock_guess_lexer.return_value = None + + now = u(int(time.time())) + config = 'tests/samples/configs/good_config.cfg' + entity = 'tests/samples/codefiles/python.py' + args = ['--file', entity, '--config', config, '--time', now, '--alternate-language', 'foo', '--plugin', 'NeoVim/703 vim-wakatime/4.0.9'] + + retval = execute(args) + self.assertEquals(retval, 102) + + language = None + self.assertEqual(self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0].get('language'), language) + + def test_error_reading_alternate_language_json_map_file(self): + response = Response() + response.status_code = 500 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + with utils.mock.patch('wakatime.stats.smart_guess_lexer') as mock_guess_lexer: + mock_guess_lexer.return_value = None + + with utils.mock.patch('wakatime.stats.open') as mock_open: + mock_open.side_effect = IOError('') + + now = u(int(time.time())) + config = 'tests/samples/configs/good_config.cfg' + entity = 'tests/samples/codefiles/python.py' + args = ['--file', entity, '--config', config, '--time', now, '--alternate-language', 'foo', '--plugin', 'NeoVim/703 vim-wakatime/4.0.9'] + + retval = execute(args) + self.assertEquals(retval, 102) + + language = None + self.assertEqual(self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0].get('language'), language) diff --git a/tests/test_main.py b/tests/test_main.py index 9fa7842..91452a5 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -368,88 +368,6 @@ class BaseTestCase(utils.TestCase): self.assertEquals(stats, json.loads(self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][1])) self.patched['wakatime.offlinequeue.Queue.pop'].assert_not_called() - def test_alternate_project(self): - response = Response() - response.status_code = 0 - self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response - - now = u(int(time.time())) - entity = 'tests/samples/codefiles/twolinefile.txt' - config = 'tests/samples/configs/good_config.cfg' - - args = ['--file', entity, '--alternate-project', 'xyz', '--config', config, '--time', now] - - retval = execute(args) - self.assertEquals(retval, API_ERROR) - self.assertEquals(sys.stdout.getvalue(), '') - self.assertEquals(sys.stderr.getvalue(), '') - - self.patched['wakatime.session_cache.SessionCache.get'].assert_called_once_with() - self.patched['wakatime.session_cache.SessionCache.delete'].assert_called_once_with() - self.patched['wakatime.session_cache.SessionCache.save'].assert_not_called() - - heartbeat = { - 'language': 'Text only', - 'lines': 2, - 'entity': os.path.abspath(entity), - 'project': os.path.basename(os.path.abspath('.')), - 'branch': os.environ.get('TRAVIS_COMMIT', ANY), - 'time': float(now), - 'type': 'file', - } - stats = { - u('cursorpos'): None, - u('dependencies'): [], - u('language'): u('Text only'), - u('lineno'): None, - u('lines'): 2, - } - - self.patched['wakatime.offlinequeue.Queue.push'].assert_called_once_with(heartbeat, ANY, None) - self.assertEquals(stats, json.loads(self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][1])) - self.patched['wakatime.offlinequeue.Queue.pop'].assert_not_called() - - def test_set_project_from_command_line(self): - response = Response() - response.status_code = 0 - self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response - - now = u(int(time.time())) - entity = 'tests/samples/codefiles/twolinefile.txt' - config = 'tests/samples/configs/good_config.cfg' - - args = ['--file', entity, '--project', 'xyz', '--config', config, '--time', now] - - retval = execute(args) - self.assertEquals(retval, API_ERROR) - self.assertEquals(sys.stdout.getvalue(), '') - self.assertEquals(sys.stderr.getvalue(), '') - - self.patched['wakatime.session_cache.SessionCache.get'].assert_called_once_with() - self.patched['wakatime.session_cache.SessionCache.delete'].assert_called_once_with() - self.patched['wakatime.session_cache.SessionCache.save'].assert_not_called() - - heartbeat = { - 'language': 'Text only', - 'lines': 2, - 'entity': os.path.abspath(entity), - 'project': 'xyz', - 'branch': os.environ.get('TRAVIS_COMMIT', ANY), - 'time': float(now), - 'type': 'file', - } - stats = { - u('cursorpos'): None, - u('dependencies'): [], - u('language'): u('Text only'), - u('lineno'): None, - u('lines'): 2, - } - - self.patched['wakatime.offlinequeue.Queue.push'].assert_called_once_with(heartbeat, ANY, None) - self.assertEquals(stats, json.loads(self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][1])) - self.patched['wakatime.offlinequeue.Queue.pop'].assert_not_called() - def test_missing_entity_file(self): response = Response() response.status_code = 201 @@ -468,7 +386,7 @@ class BaseTestCase(utils.TestCase): self.patched['wakatime.session_cache.SessionCache.save'].assert_not_called() self.patched['wakatime.offlinequeue.Queue.push'].assert_not_called() - self.patched['wakatime.offlinequeue.Queue.pop'].assert_not_called() + self.patched['wakatime.offlinequeue.Queue.pop'].assert_called_once_with() def test_proxy_argument(self): response = Response() @@ -653,19 +571,23 @@ class BaseTestCase(utils.TestCase): response.status_code = 201 self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response - entity = 'tests/samples/codefiles/emptyfile.txt' + project1 = os.path.basename(os.path.abspath('.')) + project2 = 'xyz' + entity1 = os.path.abspath('tests/samples/codefiles/emptyfile.txt') + entity2 = os.path.abspath('tests/samples/codefiles/twolinefile.txt') config = 'tests/samples/configs/good_config.cfg' - args = ['--file', entity, '--config', config, '--extra-heartbeats'] + args = ['--file', entity1, '--config', config, '--extra-heartbeats'] with utils.mock.patch('wakatime.main.sys.stdin') as mock_stdin: now = int(time.time()) heartbeats = json.dumps([{ 'timestamp': now, - 'entity': entity, + 'entity': entity2, 'entity_type': 'file', + 'project': project2, 'is_write': True, }]) - mock_stdin.read.return_value = heartbeats + mock_stdin.readline.return_value = heartbeats retval = execute(args) @@ -678,4 +600,16 @@ class BaseTestCase(utils.TestCase): self.patched['wakatime.session_cache.SessionCache.save'].assert_has_calls([call(ANY), call(ANY)]) self.patched['wakatime.offlinequeue.Queue.push'].assert_not_called() - self.patched['wakatime.offlinequeue.Queue.pop'].assert_has_calls([call(), call()]) + self.patched['wakatime.offlinequeue.Queue.pop'].assert_called_once_with() + + calls = self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].call_args_list + + body = calls[0][0][0].body + data = json.loads(body) + self.assertEquals(data.get('entity'), entity1) + self.assertEquals(data.get('project'), project1) + + body = calls[1][0][0].body + data = json.loads(body) + self.assertEquals(data.get('entity'), entity2) + self.assertEquals(data.get('project'), project2) diff --git a/tests/test_project.py b/tests/test_project.py index 6ccf81c..36b26e8 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -55,6 +55,60 @@ class LanguagesTestCase(utils.TestCase): self.assertEquals('forced-project', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + def test_alternate_project_argument_does_not_override_detected_project(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + now = u(int(time.time())) + entity = 'tests/samples/projects/git/emptyfile.txt' + config = 'tests/samples/configs/good_config.cfg' + project = os.path.basename(os.path.abspath('.')) + + args = ['--alternate-project', 'alt-project', '--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals(project, self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + + def test_alternate_project_argument_does_not_override_project_argument(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + now = u(int(time.time())) + entity = 'tests/samples/projects/git/emptyfile.txt' + config = 'tests/samples/configs/good_config.cfg' + + args = ['--project', 'forced-project', '--alternate-project', 'alt-project', '--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals('forced-project', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + + def test_alternate_project_argument_used_when_project_not_detected(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + tempdir = tempfile.mkdtemp() + entity = 'tests/samples/projects/git/emptyfile.txt' + shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) + + now = u(int(time.time())) + entity = os.path.join(tempdir, 'emptyfile.txt') + config = 'tests/samples/configs/good_config.cfg' + + args = ['--file', entity, '--config', config, '--time', now] + execute(args) + + args = ['--file', entity, '--config', config, '--time', now, '--alternate-project', 'alt-project'] + execute(args) + + calls = self.patched['wakatime.offlinequeue.Queue.push'].call_args_list + self.assertEquals(None, calls[0][0][0].get('project')) + self.assertEquals('alt-project', calls[1][0][0]['project']) + def test_wakatime_project_file(self): response = Response() response.status_code = 0 diff --git a/wakatime/constants.py b/wakatime/constants.py index 9db227f..1c7316b 100644 --- a/wakatime/constants.py +++ b/wakatime/constants.py @@ -15,3 +15,4 @@ API_ERROR = 102 CONFIG_FILE_PARSE_ERROR = 103 AUTH_ERROR = 104 UNKNOWN_ERROR = 105 +MALFORMED_HEARTBEAT_ERROR = 106 diff --git a/wakatime/logger.py b/wakatime/logger.py index 78b38ee..3d3f166 100644 --- a/wakatime/logger.py +++ b/wakatime/logger.py @@ -41,10 +41,10 @@ class CustomEncoder(json.JSONEncoder): class JsonFormatter(logging.Formatter): - def setup(self, timestamp, isWrite, entity, version, plugin, verbose, + def setup(self, timestamp, is_write, entity, version, plugin, verbose, warnings=False): self.timestamp = timestamp - self.isWrite = isWrite + self.is_write = is_write self.entity = entity self.version = version self.plugin = plugin @@ -61,10 +61,10 @@ class JsonFormatter(logging.Formatter): if self.verbose: data['caller'] = record.pathname data['lineno'] = record.lineno - data['isWrite'] = self.isWrite + data['is_write'] = self.is_write data['file'] = self.entity - if not self.isWrite: - del data['isWrite'] + if not self.is_write: + del data['is_write'] data['level'] = record.levelname data['message'] = record.getMessage() if self.warnings else record.msg if not self.plugin: @@ -103,7 +103,7 @@ def setup_logging(args, version): formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z') formatter.setup( timestamp=args.timestamp, - isWrite=args.isWrite, + is_write=args.is_write, entity=args.entity, version=version, plugin=args.plugin, @@ -118,7 +118,7 @@ def setup_logging(args, version): warnings_formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z') warnings_formatter.setup( timestamp=args.timestamp, - isWrite=args.isWrite, + is_write=args.is_write, entity=args.entity, version=version, plugin=args.plugin, diff --git a/wakatime/main.py b/wakatime/main.py index c2a12e9..ef104dd 100644 --- a/wakatime/main.py +++ b/wakatime/main.py @@ -37,6 +37,7 @@ from .constants import ( CONFIG_FILE_PARSE_ERROR, SUCCESS, UNKNOWN_ERROR, + MALFORMED_HEARTBEAT_ERROR, ) from .logger import setup_logging from .offlinequeue import Queue @@ -107,7 +108,7 @@ def parseArguments(): parser.add_argument('--key', dest='key', help='your wakatime api key; uses api_key from '+ '~/.wakatime.cfg by default') - parser.add_argument('--write', dest='isWrite', + parser.add_argument('--write', dest='is_write', action='store_true', help='when set, tells api this heartbeat was triggered from '+ 'writing to a file') @@ -299,7 +300,7 @@ def get_user_agent(plugin): def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, - entity=None, timestamp=None, isWrite=None, plugin=None, + entity=None, timestamp=None, is_write=None, plugin=None, offline=None, entity_type='file', hidefilenames=None, proxy=None, api_url=None, timeout=None, **kwargs): """Sends heartbeat as POST request to WakaTime api server. @@ -331,8 +332,8 @@ def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, data['lineno'] = stats['lineno'] if stats.get('cursorpos'): data['cursorpos'] = stats['cursorpos'] - if isWrite: - data['is_write'] = isWrite + if is_write: + data['is_write'] = is_write if project: data['project'] = project if branch: @@ -439,7 +440,7 @@ def sync_offline_heartbeats(args, hostname): hostname=hostname, stats=json.loads(heartbeat['stats']), key=args.key, - isWrite=heartbeat['is_write'], + is_write=heartbeat['is_write'], plugin=heartbeat['plugin'], offline=args.offline, hidefilenames=args.hidefilenames, @@ -455,41 +456,36 @@ def sync_offline_heartbeats(args, hostname): return SUCCESS -def process_heartbeat(args, configs, heartbeat): - exclude = should_exclude(args.entity, args.include, args.exclude) +def process_heartbeat(args, configs, hostname, heartbeat): + exclude = should_exclude(heartbeat['entity'], args.include, args.exclude) if exclude is not False: log.debug(u('Skipping because matches exclude pattern: {pattern}').format( pattern=u(exclude), )) return SUCCESS - if args.entity_type != 'file' or os.path.isfile(args.entity): + if heartbeat['entity_type'] != 'file' or os.path.isfile(heartbeat['entity']): - stats = get_file_stats(args.entity, - entity_type=args.entity_type, - lineno=args.lineno, - cursorpos=args.cursorpos, + stats = get_file_stats(heartbeat['entity'], + entity_type=heartbeat['entity_type'], + lineno=heartbeat.get('lineno'), + cursorpos=heartbeat.get('cursorpos'), plugin=args.plugin, - alternate_language=args.alternate_language) + alternate_language=heartbeat.get('alternate_language')) - project = args.project or args.alternate_project + project = heartbeat.get('project') or heartbeat.get('alternate_project') branch = None - if args.entity_type == 'file': - project, branch = get_project_info(configs, args) + if heartbeat['entity_type'] == 'file': + project, branch = get_project_info(configs, heartbeat) - kwargs = vars(args) - kwargs['project'] = project - kwargs['branch'] = branch - kwargs['stats'] = stats - hostname = args.hostname or socket.gethostname() - kwargs['hostname'] = hostname - kwargs['timeout'] = args.timeout + heartbeat['project'] = project + heartbeat['branch'] = branch + heartbeat['stats'] = stats + heartbeat['hostname'] = hostname + heartbeat['timeout'] = args.timeout + heartbeat['key'] = args.key - status = send_heartbeat(**kwargs) - if status == SUCCESS: - return sync_offline_heartbeats(args, hostname) - else: - return status + return send_heartbeat(**heartbeat) else: log.debug('File does not exist; ignoring this heartbeat.') @@ -508,23 +504,20 @@ def execute(argv=None): try: - heartbeat = { - 'timestamp': args.timestamp, - 'entity': args.entity, - 'entity_type': args.entity_type, - 'project': args.project, - 'is_write': args.isWrite, - 'lineno': args.lineno, - 'cursorpos': args.cursorpos, - 'alternate_project': args.alternate_project, - 'alternate_language': args.alternate_language, - } - retval = process_heartbeat(args, configs, heartbeat) + hostname = args.hostname or socket.gethostname() + + heartbeat = vars(args) + retval = process_heartbeat(args, configs, hostname, heartbeat) if args.extra_heartbeats: - heartbeats = json.loads(sys.stdin.read()) - for heartbeat in heartbeats: - retval = process_heartbeat(args, configs, heartbeat) + try: + for heartbeat in json.loads(sys.stdin.readline()): + retval = process_heartbeat(args, configs, hostname, heartbeat) + except json.JSONDecodeError: + retval = MALFORMED_HEARTBEAT_ERROR + + if retval == SUCCESS: + retval = sync_offline_heartbeats(args, hostname) return retval diff --git a/wakatime/project.py b/wakatime/project.py index f3299fe..c008651 100644 --- a/wakatime/project.py +++ b/wakatime/project.py @@ -33,7 +33,7 @@ REV_CONTROL_PLUGINS = [ ] -def get_project_info(configs, args): +def get_project_info(configs, heartbeat): """Find the current project and branch. First looks for a .wakatime-project file. Second, uses the --project arg. @@ -50,14 +50,14 @@ def get_project_info(configs, args): plugin_name = plugin_cls.__name__.lower() plugin_configs = get_configs_for_plugin(plugin_name, configs) - project = plugin_cls(args.entity, configs=plugin_configs) + project = plugin_cls(heartbeat['entity'], configs=plugin_configs) if project.process(): project_name = project_name or project.name() branch_name = project.branch() break if project_name is None: - project_name = args.project + project_name = heartbeat.get('project') if project_name is None or branch_name is None: @@ -66,14 +66,14 @@ def get_project_info(configs, args): plugin_name = plugin_cls.__name__.lower() plugin_configs = get_configs_for_plugin(plugin_name, configs) - project = plugin_cls(args.entity, configs=plugin_configs) + project = plugin_cls(heartbeat['entity'], configs=plugin_configs) if project.process(): project_name = project_name or project.name() branch_name = branch_name or project.branch() break if project_name is None: - project_name = args.alternate_project + project_name = heartbeat.get('alternate_project') return project_name, branch_name