{"action":"created","comment":{"url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/comments/73636224","id":73636224,"diff_hunk":"@@ -0,0 +1,128 @@\n+# -*- coding: utf-8 -*-\n+\n+\n+import os\n+import sqlite3\n+import subprocess\n+\n+\n+from helpers.file_utils import FileUtils\n+\n+\n+class DBUtils(object):\n+    \"\"\"Provides methods for creating and comparing sqlite databases.\"\"\"\n+\n+    def __init__(self):\n+        self.fileutils = FileUtils()\n+\n+    def clean_up(self):\n+        self.fileutils.clean_up()\n+\n+    def create_sqlite_db_with_sql(self, sql_string):\n+        \"\"\"\n+        Creates an SQLite db and executes the passed sql statements on it.\n+\n+        :param sql_string: the sql statements to execute on the newly created\n+                           db\n+        :return: the path to the created db file\n+        \"\"\"\n+\n+        db_path = self.fileutils.create_file_path(suffix=\".anki2\")\n+        connection = sqlite3.connect(db_path)\n+        cursor = connection.cursor()\n+        cursor.executescript(sql_string)\n+        connection.commit()\n+        connection.close()\n+\n+        return db_path\n+\n+    @staticmethod\n+    def sqlite_db_to_sql_string(database):\n+        \"\"\"\n+        Returns a string containing the sql export of the database. Used for\n+        debugging.\n+\n+        :param database: either the path to the SQLite db file or an open\n+                         connection to it\n+        :return: a string representing the sql export of the database\n+        \"\"\"\n+\n+        if type(database) == str:\n+            connection = sqlite3.connect(database)\n+        else:\n+            connection = database\n+\n+        res = '\\n'.join(connection.iterdump())\n+\n+        if type(database) == str:\n+            connection.close()\n+\n+        return res\n+\n+    def media_dbs_differ(self, left_db_path, right_db_path, compare_timestamps=False):\n+        \"\"\"\n+        Compares two media sqlite database files for equality. mtime and dirMod\n+        timestamps are not considered when comparing.\n+\n+        :param left_db_path: path to the left db file\n+        :param right_db_path: path to the right db file\n+        :param compare_timestamps: flag determining if timestamp values\n+                                   (media.mtime and meta.dirMod) are included\n+                                   in the comparison\n+        :return: True if the specified databases differ, False else\n+        \"\"\"\n+\n+        if not os.path.isfile(left_db_path):\n+            raise IOError(\"file '\" + left_db_path + \"' does not exist\")\n+        elif not os.path.isfile(right_db_path):\n+            raise IOError(\"file '\" + right_db_path + \"' does not exist\")\n+\n+        # Create temporary copies of the files to act on.\n+        left_db_path = self.fileutils.create_file_copy(left_db_path)\n+        right_db_path = self.fileutils.create_file_copy(right_db_path)\n+\n+        if not compare_timestamps:\n+            # Set all timestamps that are not NULL to 0.\n+            for dbPath in [left_db_path, right_db_path]:\n+                connection = sqlite3.connect(dbPath)\n+\n+                connection.execute(\"\"\"UPDATE media SET mtime=0\n+                                      WHERE mtime IS NOT NULL\"\"\")\n+\n+                connection.execute(\"\"\"UPDATE meta SET dirMod=0\n+                                      WHERE rowid=1\"\"\")\n+                connection.commit()\n+                connection.close()\n+\n+        return self.__sqlite_dbs_differ(left_db_path, right_db_path)\n+\n+    def __sqlite_dbs_differ(self, left_db_path, right_db_path):\n+        \"\"\"\n+        Uses the sqldiff cli tool to compare two sqlite files for equality.\n+        Returns True if the databases differ, False if they don't.\n+\n+        :param left_db_path: path to the left db file\n+        :param right_db_path: path to the right db file\n+        :return: True if the specified databases differ, False else\n+        \"\"\"\n+\n+        command = [\"/bin/sqldiff\", left_db_path, right_db_path]","path":"tests/helpers/db_utils.py","position":109,"original_position":109,"commit_id":"8082acf4406d6c3c594ee945035cd06d1a95e640","original_commit_id":"8082acf4406d6c3c594ee945035cd06d1a95e640","user":{"login":"dsnopek","id":191561,"avatar_url":"https://avatars.githubusercontent.com/u/191561?v=3","gravatar_id":"","url":"https://api.github.com/users/dsnopek","html_url":"https://github.com/dsnopek","followers_url":"https://api.github.com/users/dsnopek/followers","following_url":"https://api.github.com/users/dsnopek/following{/other_user}","gists_url":"https://api.github.com/users/dsnopek/gists{/gist_id}","starred_url":"https://api.github.com/users/dsnopek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dsnopek/subscriptions","organizations_url":"https://api.github.com/users/dsnopek/orgs","repos_url":"https://api.github.com/users/dsnopek/repos","events_url":"https://api.github.com/users/dsnopek/events{/privacy}","received_events_url":"https://api.github.com/users/dsnopek/received_events","type":"User","site_admin":false},"body":"I'm not crazy about adding the dependency on 'sqldiff'. Since we're dealing with a limited number of tables, couldn't we just query the data into data structures and compare that rather than comparing the whole databases?","created_at":"2016-08-05T03:00:23Z","updated_at":"2016-08-05T03:00:23Z","html_url":"https://github.com/dsnopek/anki-sync-server/pull/35#discussion_r73636224","pull_request_url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35","_links":{"self":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/comments/73636224"},"html":{"href":"https://github.com/dsnopek/anki-sync-server/pull/35#discussion_r73636224"},"pull_request":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35"}}},"pull_request":{"url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35","id":58492116,"html_url":"https://github.com/dsnopek/anki-sync-server/pull/35","diff_url":"https://github.com/dsnopek/anki-sync-server/pull/35.diff","patch_url":"https://github.com/dsnopek/anki-sync-server/pull/35.patch","issue_url":"https://api.github.com/repos/dsnopek/anki-sync-server/issues/35","number":35,"state":"open","locked":false,"title":"Add helper classes for integration tests, refactor user managers and ankiserverctl.py","user":{"login":"cdpm","id":7420055,"avatar_url":"https://avatars.githubusercontent.com/u/7420055?v=3","gravatar_id":"","url":"https://api.github.com/users/cdpm","html_url":"https://github.com/cdpm","followers_url":"https://api.github.com/users/cdpm/followers","following_url":"https://api.github.com/users/cdpm/following{/other_user}","gists_url":"https://api.github.com/users/cdpm/gists{/gist_id}","starred_url":"https://api.github.com/users/cdpm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cdpm/subscriptions","organizations_url":"https://api.github.com/users/cdpm/orgs","repos_url":"https://api.github.com/users/cdpm/repos","events_url":"https://api.github.com/users/cdpm/events{/privacy}","received_events_url":"https://api.github.com/users/cdpm/received_events","type":"User","site_admin":false},"body":"Add test helper classes for creating and managing temporary files, working with anki collections and sqlite dbs. Add class for managing users so users can be added programmatically without using ankiserverctl.","created_at":"2016-02-05T22:15:32Z","updated_at":"2016-08-05T03:00:23Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e08ae92f2da843188ca088d5908160f2d8c67b0b","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35/commits","review_comments_url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35/comments","review_comment_url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dsnopek/anki-sync-server/issues/35/comments","statuses_url":"https://api.github.com/repos/dsnopek/anki-sync-server/statuses/8082acf4406d6c3c594ee945035cd06d1a95e640","head":{"label":"cdpm:test_helpers","ref":"test_helpers","sha":"8082acf4406d6c3c594ee945035cd06d1a95e640","user":{"login":"cdpm","id":7420055,"avatar_url":"https://avatars.githubusercontent.com/u/7420055?v=3","gravatar_id":"","url":"https://api.github.com/users/cdpm","html_url":"https://github.com/cdpm","followers_url":"https://api.github.com/users/cdpm/followers","following_url":"https://api.github.com/users/cdpm/following{/other_user}","gists_url":"https://api.github.com/users/cdpm/gists{/gist_id}","starred_url":"https://api.github.com/users/cdpm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cdpm/subscriptions","organizations_url":"https://api.github.com/users/cdpm/orgs","repos_url":"https://api.github.com/users/cdpm/repos","events_url":"https://api.github.com/users/cdpm/events{/privacy}","received_events_url":"https://api.github.com/users/cdpm/received_events","type":"User","site_admin":false},"repo":{"id":49092442,"name":"anki-sync-server","full_name":"cdpm/anki-sync-server","owner":{"login":"cdpm","id":7420055,"avatar_url":"https://avatars.githubusercontent.com/u/7420055?v=3","gravatar_id":"","url":"https://api.github.com/users/cdpm","html_url":"https://github.com/cdpm","followers_url":"https://api.github.com/users/cdpm/followers","following_url":"https://api.github.com/users/cdpm/following{/other_user}","gists_url":"https://api.github.com/users/cdpm/gists{/gist_id}","starred_url":"https://api.github.com/users/cdpm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cdpm/subscriptions","organizations_url":"https://api.github.com/users/cdpm/orgs","repos_url":"https://api.github.com/users/cdpm/repos","events_url":"https://api.github.com/users/cdpm/events{/privacy}","received_events_url":"https://api.github.com/users/cdpm/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/cdpm/anki-sync-server","description":"A personal Anki sync server (so you can sync against your own server rather than AnkiWeb)","fork":true,"url":"https://api.github.com/repos/cdpm/anki-sync-server","forks_url":"https://api.github.com/repos/cdpm/anki-sync-server/forks","keys_url":"https://api.github.com/repos/cdpm/anki-sync-server/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cdpm/anki-sync-server/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cdpm/anki-sync-server/teams","hooks_url":"https://api.github.com/repos/cdpm/anki-sync-server/hooks","issue_events_url":"https://api.github.com/repos/cdpm/anki-sync-server/issues/events{/number}","events_url":"https://api.github.com/repos/cdpm/anki-sync-server/events","assignees_url":"https://api.github.com/repos/cdpm/anki-sync-server/assignees{/user}","branches_url":"https://api.github.com/repos/cdpm/anki-sync-server/branches{/branch}","tags_url":"https://api.github.com/repos/cdpm/anki-sync-server/tags","blobs_url":"https://api.github.com/repos/cdpm/anki-sync-server/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cdpm/anki-sync-server/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cdpm/anki-sync-server/git/refs{/sha}","trees_url":"https://api.github.com/repos/cdpm/anki-sync-server/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cdpm/anki-sync-server/statuses/{sha}","languages_url":"https://api.github.com/repos/cdpm/anki-sync-server/languages","stargazers_url":"https://api.github.com/repos/cdpm/anki-sync-server/stargazers","contributors_url":"https://api.github.com/repos/cdpm/anki-sync-server/contributors","subscribers_url":"https://api.github.com/repos/cdpm/anki-sync-server/subscribers","subscription_url":"https://api.github.com/repos/cdpm/anki-sync-server/subscription","commits_url":"https://api.github.com/repos/cdpm/anki-sync-server/commits{/sha}","git_commits_url":"https://api.github.com/repos/cdpm/anki-sync-server/git/commits{/sha}","comments_url":"https://api.github.com/repos/cdpm/anki-sync-server/comments{/number}","issue_comment_url":"https://api.github.com/repos/cdpm/anki-sync-server/issues/comments{/number}","contents_url":"https://api.github.com/repos/cdpm/anki-sync-server/contents/{+path}","compare_url":"https://api.github.com/repos/cdpm/anki-sync-server/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cdpm/anki-sync-server/merges","archive_url":"https://api.github.com/repos/cdpm/anki-sync-server/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cdpm/anki-sync-server/downloads","issues_url":"https://api.github.com/repos/cdpm/anki-sync-server/issues{/number}","pulls_url":"https://api.github.com/repos/cdpm/anki-sync-server/pulls{/number}","milestones_url":"https://api.github.com/repos/cdpm/anki-sync-server/milestones{/number}","notifications_url":"https://api.github.com/repos/cdpm/anki-sync-server/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cdpm/anki-sync-server/labels{/name}","releases_url":"https://api.github.com/repos/cdpm/anki-sync-server/releases{/id}","deployments_url":"https://api.github.com/repos/cdpm/anki-sync-server/deployments","created_at":"2016-01-05T21:00:33Z","updated_at":"2016-01-19T10:32:18Z","pushed_at":"2016-08-02T17:35:07Z","git_url":"git://github.com/cdpm/anki-sync-server.git","ssh_url":"git@github.com:cdpm/anki-sync-server.git","clone_url":"https://github.com/cdpm/anki-sync-server.git","svn_url":"https://github.com/cdpm/anki-sync-server","homepage":null,"size":214,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"dsnopek:master","ref":"master","sha":"07eaac800610ab9bfb6622e3203d55abdadd3399","user":{"login":"dsnopek","id":191561,"avatar_url":"https://avatars.githubusercontent.com/u/191561?v=3","gravatar_id":"","url":"https://api.github.com/users/dsnopek","html_url":"https://github.com/dsnopek","followers_url":"https://api.github.com/users/dsnopek/followers","following_url":"https://api.github.com/users/dsnopek/following{/other_user}","gists_url":"https://api.github.com/users/dsnopek/gists{/gist_id}","starred_url":"https://api.github.com/users/dsnopek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dsnopek/subscriptions","organizations_url":"https://api.github.com/users/dsnopek/orgs","repos_url":"https://api.github.com/users/dsnopek/repos","events_url":"https://api.github.com/users/dsnopek/events{/privacy}","received_events_url":"https://api.github.com/users/dsnopek/received_events","type":"User","site_admin":false},"repo":{"id":9199887,"name":"anki-sync-server","full_name":"dsnopek/anki-sync-server","owner":{"login":"dsnopek","id":191561,"avatar_url":"https://avatars.githubusercontent.com/u/191561?v=3","gravatar_id":"","url":"https://api.github.com/users/dsnopek","html_url":"https://github.com/dsnopek","followers_url":"https://api.github.com/users/dsnopek/followers","following_url":"https://api.github.com/users/dsnopek/following{/other_user}","gists_url":"https://api.github.com/users/dsnopek/gists{/gist_id}","starred_url":"https://api.github.com/users/dsnopek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dsnopek/subscriptions","organizations_url":"https://api.github.com/users/dsnopek/orgs","repos_url":"https://api.github.com/users/dsnopek/repos","events_url":"https://api.github.com/users/dsnopek/events{/privacy}","received_events_url":"https://api.github.com/users/dsnopek/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/dsnopek/anki-sync-server","description":"A personal Anki sync server (so you can sync against your own server rather than AnkiWeb)","fork":false,"url":"https://api.github.com/repos/dsnopek/anki-sync-server","forks_url":"https://api.github.com/repos/dsnopek/anki-sync-server/forks","keys_url":"https://api.github.com/repos/dsnopek/anki-sync-server/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dsnopek/anki-sync-server/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dsnopek/anki-sync-server/teams","hooks_url":"https://api.github.com/repos/dsnopek/anki-sync-server/hooks","issue_events_url":"https://api.github.com/repos/dsnopek/anki-sync-server/issues/events{/number}","events_url":"https://api.github.com/repos/dsnopek/anki-sync-server/events","assignees_url":"https://api.github.com/repos/dsnopek/anki-sync-server/assignees{/user}","branches_url":"https://api.github.com/repos/dsnopek/anki-sync-server/branches{/branch}","tags_url":"https://api.github.com/repos/dsnopek/anki-sync-server/tags","blobs_url":"https://api.github.com/repos/dsnopek/anki-sync-server/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dsnopek/anki-sync-server/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dsnopek/anki-sync-server/git/refs{/sha}","trees_url":"https://api.github.com/repos/dsnopek/anki-sync-server/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dsnopek/anki-sync-server/statuses/{sha}","languages_url":"https://api.github.com/repos/dsnopek/anki-sync-server/languages","stargazers_url":"https://api.github.com/repos/dsnopek/anki-sync-server/stargazers","contributors_url":"https://api.github.com/repos/dsnopek/anki-sync-server/contributors","subscribers_url":"https://api.github.com/repos/dsnopek/anki-sync-server/subscribers","subscription_url":"https://api.github.com/repos/dsnopek/anki-sync-server/subscription","commits_url":"https://api.github.com/repos/dsnopek/anki-sync-server/commits{/sha}","git_commits_url":"https://api.github.com/repos/dsnopek/anki-sync-server/git/commits{/sha}","comments_url":"https://api.github.com/repos/dsnopek/anki-sync-server/comments{/number}","issue_comment_url":"https://api.github.com/repos/dsnopek/anki-sync-server/issues/comments{/number}","contents_url":"https://api.github.com/repos/dsnopek/anki-sync-server/contents/{+path}","compare_url":"https://api.github.com/repos/dsnopek/anki-sync-server/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dsnopek/anki-sync-server/merges","archive_url":"https://api.github.com/repos/dsnopek/anki-sync-server/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dsnopek/anki-sync-server/downloads","issues_url":"https://api.github.com/repos/dsnopek/anki-sync-server/issues{/number}","pulls_url":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls{/number}","milestones_url":"https://api.github.com/repos/dsnopek/anki-sync-server/milestones{/number}","notifications_url":"https://api.github.com/repos/dsnopek/anki-sync-server/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dsnopek/anki-sync-server/labels{/name}","releases_url":"https://api.github.com/repos/dsnopek/anki-sync-server/releases{/id}","deployments_url":"https://api.github.com/repos/dsnopek/anki-sync-server/deployments","created_at":"2013-04-03T16:53:02Z","updated_at":"2016-07-20T05:54:21Z","pushed_at":"2016-08-03T12:09:36Z","git_url":"git://github.com/dsnopek/anki-sync-server.git","ssh_url":"git@github.com:dsnopek/anki-sync-server.git","clone_url":"https://github.com/dsnopek/anki-sync-server.git","svn_url":"https://github.com/dsnopek/anki-sync-server","homepage":null,"size":280,"stargazers_count":180,"watchers_count":180,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":29,"mirror_url":null,"open_issues_count":15,"forks":29,"open_issues":15,"watchers":180,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35"},"html":{"href":"https://github.com/dsnopek/anki-sync-server/pull/35"},"issue":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/issues/35"},"comments":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/issues/35/comments"},"review_comments":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35/comments"},"review_comment":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/pulls/35/commits"},"statuses":{"href":"https://api.github.com/repos/dsnopek/anki-sync-server/statuses/8082acf4406d6c3c594ee945035cd06d1a95e640"}}}}
{"actor":{"display_login":"dsnopek"}}