PageRenderTime 171ms CodeModel.GetById 55ms RepoModel.GetById 0ms app.codeStats 0ms

/ckan/tests/legacy/test_coding_standards.py

https://gitlab.com/iislod/ckan
Python | 924 lines | 899 code | 7 blank | 18 comment | 11 complexity | 70c8e77aea70873aaa2b29c0786fceeb MD5 | raw file
  1. '''
  2. The aim of these tests is to check and improve the coding standards in ckan.
  3. Common issues are tested for here and tests fail if they are discovered in
  4. files that are either new or were previously good. Bad files are
  5. blacklisted to prevent them throwing errors in many cases because of the
  6. number of affected files e.g. PEP8. However if files start to pass a test
  7. will fail and the file should be removed from the blacklist so that it will
  8. then be kept clean in future.
  9. The idea is to slowly improve the code quality in ckan without having files
  10. deteriourating when they do reach the required standard.
  11. Please do not add new files to the list as any new files should meet the
  12. current coding standards. Please add comments by files that fail if there
  13. are legitamate reasons for the failure.
  14. '''
  15. import sys
  16. import os
  17. import re
  18. import cStringIO
  19. import inspect
  20. import itertools
  21. import pep8
  22. file_path = os.path.dirname(__file__)
  23. base_path = os.path.abspath(os.path.join(file_path, '..', '..', '..'))
  24. def process_directory(directory, ext='.py'):
  25. base_len = len(base_path) + 1
  26. for (dirpath, dirnames, filenames) in os.walk(directory):
  27. # ignore hidden files and dir
  28. filenames = [f for f in filenames if not f[0] == '.']
  29. dirnames[:] = [d for d in dirnames if not d[0] == '.']
  30. for name in filenames:
  31. if name.endswith(ext):
  32. path = os.path.join(dirpath, name)
  33. filename = path[base_len:]
  34. yield path, filename
  35. def output_errors(filename, errors):
  36. out = ['']
  37. out.append('-' * len(filename))
  38. out.append(filename)
  39. out.append('-' * len(filename))
  40. for error in errors:
  41. out.append(error)
  42. return '\n'.join(out)
  43. def show_fails(msg, errors):
  44. if errors:
  45. msg = ['\n%s' % msg]
  46. for error in errors:
  47. msg.append(errors[error])
  48. msg.append('\n\nFailing Files:\n==============')
  49. msg += sorted(errors)
  50. raise Exception('\n'.join(msg))
  51. def show_passing(msg, errors):
  52. if errors:
  53. raise Exception('\n%s\n\n' % msg + '\n'.join(sorted(errors)))
  54. def cs_filter(f, filter_, ignore_comment_lines=True):
  55. ''' filter the file removing comments if requested.
  56. looks for comments like
  57. # CS: <filter_> ignore
  58. # CS: <filter_> ignore x line
  59. and removes the requested number of lines. Lines are removed by
  60. blanking so the line numbers reported will be correct. This allows us
  61. to check files that have known violations of the test rules. '''
  62. # this RegEx is of poor quality but works
  63. exp = r'^\s*#\s+CS:.*%s.*ignore\D*((\d+)\s+line)*'
  64. re_ignore = re.compile(exp % filter_)
  65. ignore = 0
  66. out = []
  67. count = 1
  68. for line in f:
  69. # ignore the line if we have been told too
  70. if ignore > 0:
  71. line = ''
  72. ignore -= 1
  73. matches = re_ignore.search(line)
  74. if matches:
  75. ignore = int(matches.group(2) or 1)
  76. # ignore comments out lines
  77. if ignore_comment_lines and line.lstrip().startswith('#'):
  78. line = ''
  79. out.append(line)
  80. count += 1
  81. return out
  82. class TestBadSpellings(object):
  83. BAD_SPELLING_BLACKLIST_FILES = []
  84. # these are the bad spellings with the correct spelling
  85. # use LOWER case
  86. BAD_SPELLINGS = {
  87. # CS: bad_spelling ignore 2 lines
  88. 'licence': 'license',
  89. 'organisation': 'organization',
  90. }
  91. fails = {}
  92. passes = []
  93. done = False
  94. @classmethod
  95. def setup(cls):
  96. if not cls.done:
  97. cls.process()
  98. cls.done = True
  99. @classmethod
  100. def process(cls):
  101. blacklist = cls.BAD_SPELLING_BLACKLIST_FILES
  102. re_bad_spelling = re.compile(
  103. r'(%s)' % '|'.join([x for x in cls.BAD_SPELLINGS]),
  104. flags=re.IGNORECASE
  105. )
  106. files = itertools.chain.from_iterable([
  107. process_directory(base_path),
  108. process_directory(base_path, ext='.rst')])
  109. for path, filename in files:
  110. f = open(path, 'r')
  111. count = 1
  112. errors = []
  113. for line in cs_filter(f, 'bad_spelling'):
  114. matches = re_bad_spelling.findall(line)
  115. if matches:
  116. bad_words = []
  117. for m in matches:
  118. if m not in bad_words:
  119. bad_words.append('%s use %s' %
  120. (m, cls.BAD_SPELLINGS[m.lower()]))
  121. bad = ', '.join(bad_words)
  122. errors.append('ln:%s \t%s\n<%s>' % (count, line[:-1], bad))
  123. count += 1
  124. if errors and not filename in blacklist:
  125. cls.fails[filename] = output_errors(filename, errors)
  126. elif not errors and filename in blacklist:
  127. cls.passes.append(filename)
  128. def test_good(self):
  129. msg = 'The following files passed bad spellings rules'
  130. msg += '\nThey need removing from the test blacklist'
  131. show_passing(msg, self.passes)
  132. def test_bad(self):
  133. msg = 'The following files have bad spellings that need fixing'
  134. show_fails(msg, self.fails)
  135. class TestNastyString(object):
  136. # CS: nasty_string ignore
  137. ''' Look for a common coding problem in ckan '..%s..' % str(x) '''
  138. # Nasty str() issues
  139. #
  140. # There are places in ckan where code is like `'...%s..' % str(..)`
  141. # these cause problems when unicode is present but can remain dormant
  142. # for a long time before the issue is apparent so try to remove these.
  143. # The value is converted to a string anyway so the str() is unneeded in
  144. # any place.
  145. NASTY_STR_BLACKLIST_FILES = []
  146. fails = {}
  147. passes = []
  148. done = False
  149. @classmethod
  150. def setup(cls):
  151. if not cls.done:
  152. cls.process()
  153. cls.done = True
  154. @classmethod
  155. def process(cls):
  156. blacklist = cls.NASTY_STR_BLACKLIST_FILES
  157. re_nasty_str = re.compile(
  158. r'''("[^"]*\%s[^"]*"|'[^']*\%s[^']*').*%.*str\('''
  159. )
  160. for path, filename in process_directory(base_path):
  161. f = open(path, 'r')
  162. count = 1
  163. errors = []
  164. for line in cs_filter(f, 'nasty_string'):
  165. if re_nasty_str.search(line):
  166. errors.append('ln:%s \t%s' % (count, line[:-1]))
  167. count += 1
  168. if errors and not filename in blacklist:
  169. cls.fails[filename] = output_errors(filename, errors)
  170. elif not errors and filename in blacklist:
  171. cls.passes.append(filename)
  172. def test_good(self):
  173. msg = 'The following files passed nasty str() rules'
  174. msg += '\nThey need removing from the test blacklist'
  175. show_passing(msg, self.passes)
  176. def test_bad(self):
  177. # CS: nasty_string ignore next 2 lines
  178. msg = ('The following files have nasty str() issues that need'
  179. ' resolving\nCode is like `\'...%s..\' % str(..)`'
  180. 'and should just be `\'...%s..\' % ..`')
  181. show_fails(msg, self.fails)
  182. class TestImportStar(object):
  183. ''' Find files using from xxx import * '''
  184. # Import * file exceptions
  185. #
  186. # The following files contain one or more `from ... import *` lines
  187. # which should not be used in ckan where possible. If the files get
  188. # fixed they should be removed from this list.
  189. #
  190. # import * is bad for many reasons and should be avoided.
  191. IMPORT_STAR_BLACKLIST_FILES = [
  192. 'ckan/lib/helpers.py',
  193. 'ckan/migration/versions/001_add_existing_tables.py',
  194. 'ckan/migration/versions/002_add_author_and_maintainer.py',
  195. 'ckan/migration/versions/003_add_user_object.py',
  196. 'ckan/migration/versions/004_add_group_object.py',
  197. 'ckan/migration/versions/005_add_authorization_tables.py',
  198. 'ckan/migration/versions/006_add_ratings.py',
  199. 'ckan/migration/versions/007_add_system_roles.py',
  200. 'ckan/migration/versions/008_update_vdm_ids.py',
  201. 'ckan/migration/versions/009_add_creation_timestamps.py',
  202. 'ckan/migration/versions/010_add_user_about.py',
  203. 'ckan/migration/versions/011_add_package_search_vector.py',
  204. 'ckan/migration/versions/012_add_resources.py',
  205. 'ckan/migration/versions/013_add_hash.py',
  206. 'ckan/migration/versions/014_hash_2.py',
  207. 'ckan/migration/versions/015_remove_state_object.py',
  208. 'ckan/migration/versions/016_uuids_everywhere.py',
  209. 'ckan/migration/versions/017_add_pkg_relationships.py',
  210. 'ckan/migration/versions/018_adjust_licenses.py',
  211. 'ckan/migration/versions/019_pkg_relationships_state.py',
  212. 'ckan/migration/versions/020_add_changeset.py',
  213. 'ckan/migration/versions/022_add_group_extras.py',
  214. 'ckan/migration/versions/023_add_harvesting.py',
  215. 'ckan/migration/versions/024_add_harvested_document.py',
  216. 'ckan/migration/versions/025_add_authorization_groups.py',
  217. 'ckan/migration/versions/026_authorization_group_user_pk.py',
  218. 'ckan/migration/versions/027_adjust_harvester.py',
  219. 'ckan/migration/versions/028_drop_harvest_source_status.py',
  220. 'ckan/migration/versions/029_version_groups.py',
  221. 'ckan/migration/versions/030_additional_user_attributes.py',
  222. 'ckan/migration/versions/031_move_openid_to_new_field.py',
  223. 'ckan/migration/versions/032_add_extra_info_field_to_resources.py',
  224. 'ckan/migration/versions/033_auth_group_user_id_add_conditional.py',
  225. 'ckan/migration/versions/034_resource_group_table.py',
  226. 'ckan/migration/versions/035_harvesting_doc_versioning.py',
  227. 'ckan/migration/versions/036_lockdown_roles.py',
  228. 'ckan/migration/versions/037_role_anon_editor.py',
  229. 'ckan/migration/versions/038_delete_migration_tables.py',
  230. 'ckan/migration/versions/039_add_expired_id_and_dates.py',
  231. 'ckan/migration/versions/040_reset_key_on_user.py',
  232. 'ckan/migration/versions/041_resource_new_fields.py',
  233. 'ckan/migration/versions/042_user_revision_indexes.py',
  234. 'ckan/migration/versions/043_drop_postgres_search.py',
  235. 'ckan/migration/versions/044_add_task_status.py',
  236. 'ckan/migration/versions/045_user_name_unique.py',
  237. 'ckan/migration/versions/046_drop_changesets.py',
  238. 'ckan/migration/versions/047_rename_package_group_member.py',
  239. 'ckan/migration/versions/048_add_activity_streams_tables.py',
  240. 'ckan/migration/versions/049_add_group_approval_status.py',
  241. 'ckan/migration/versions/050_term_translation_table.py',
  242. 'ckan/migration/versions/051_add_tag_vocabulary.py',
  243. 'ckan/migration/versions/052_update_member_capacities.py',
  244. 'ckan/migration/versions/053_add_group_logo.py',
  245. 'ckan/migration/versions/056_add_related_table.py',
  246. 'ckan/migration/versions/057_tracking.py',
  247. 'ckan/migration/versions/058_add_follower_tables.py',
  248. 'ckan/migration/versions/059_add_related_count_and_flag.py',
  249. 'ckan/migration/versions/060_add_system_info_table.py',
  250. 'ckan/migration/versions/061_add_follower__group_table.py',
  251. 'ckan/migration/versions/062_add_dashboard_table.py',
  252. 'ckan/migration/versions/063_org_changes.py',
  253. 'ckan/migration/versions/064_add_email_last_sent_column.py',
  254. 'ckan/migration/versions/065_add_email_notifications_preference.py',
  255. 'ckan/new_authz.py',
  256. 'ckan/plugins/__init__.py',
  257. 'ckan/tests/legacy/functional/api/base.py',
  258. 'ckan/tests/legacy/functional/api/test_api.py',
  259. 'ckan/tests/legacy/functional/api/test_misc.py',
  260. 'ckan/tests/legacy/functional/api/test_package_search.py',
  261. 'ckan/tests/legacy/functional/api/test_resource_search.py',
  262. 'ckan/tests/legacy/functional/api/test_revision_search.py',
  263. 'ckan/tests/legacy/functional/test_group.py',
  264. 'ckan/tests/legacy/functional/test_home.py',
  265. 'ckan/tests/legacy/functional/test_package.py',
  266. 'ckan/tests/legacy/functional/test_package_relationships.py',
  267. 'ckan/tests/legacy/functional/test_tag.py',
  268. 'ckan/tests/legacy/lib/test_alphabet_pagination.py',
  269. 'ckan/tests/legacy/lib/test_helpers.py',
  270. 'ckan/tests/legacy/lib/test_resource_search.py',
  271. 'ckan/tests/legacy/lib/test_tag_search.py',
  272. 'ckan/tests/legacy/misc/test_sync.py',
  273. 'ckan/tests/legacy/models/test_extras.py',
  274. 'ckan/tests/legacy/models/test_misc.py',
  275. 'ckan/tests/legacy/models/test_package.py',
  276. 'ckan/tests/legacy/models/test_package_relationships.py',
  277. 'ckan/tests/legacy/models/test_purge_revision.py',
  278. 'ckan/tests/legacy/models/test_resource.py',
  279. 'ckan/tests/legacy/models/test_revision.py',
  280. 'ckan/tests/legacy/models/test_user.py',
  281. 'ckan/tests/legacy/pylons_controller.py',
  282. 'fabfile.py',
  283. ]
  284. fails = {}
  285. passes = []
  286. done = False
  287. @classmethod
  288. def setup(cls):
  289. if not cls.done:
  290. cls.process()
  291. cls.done = True
  292. @classmethod
  293. def process(cls):
  294. blacklist = cls.IMPORT_STAR_BLACKLIST_FILES
  295. re_import_star = re.compile(r'^\s*from\s+.*\simport\s+\*')
  296. for path, filename in process_directory(base_path):
  297. f = open(path, 'r')
  298. count = 1
  299. errors = []
  300. for line in f:
  301. if re_import_star.search(line):
  302. errors.append('%s ln:%s import *\n\t%s'
  303. % (filename, count, line))
  304. count += 1
  305. if errors and not filename in blacklist:
  306. cls.fails[filename] = output_errors(filename, errors)
  307. elif not errors and filename in blacklist:
  308. cls.passes.append(filename)
  309. def test_import_good(self):
  310. msg = 'The following files passed import * rules'
  311. msg += '\nThey need removing from the test blacklist'
  312. show_passing(msg, self.passes)
  313. def test_import_bad(self):
  314. msg = ('The following files have import * issues that need resolving\n'
  315. '`from ... import *` lines which should not be used in ckan'
  316. ' where possible.')
  317. show_fails(msg, self.fails)
  318. class TestPep8(object):
  319. ''' Check that .py files are pep8 compliant '''
  320. # PEP8 File exceptions
  321. #
  322. # The following files have known PEP8 errors. When the files get to a
  323. # point of not having any such errors they should be removed from this
  324. # list to prevent new errors being added to the file.
  325. PEP8_BLACKLIST_FILES = [
  326. 'bin/running_stats.py',
  327. 'ckan/__init__.py',
  328. 'ckan/ckan_nose_plugin.py',
  329. 'ckan/config/middleware.py',
  330. 'ckan/config/routing.py',
  331. 'ckan/config/sp_config.py',
  332. 'ckan/controllers/admin.py',
  333. 'ckan/controllers/revision.py',
  334. 'ckan/exceptions.py',
  335. 'ckan/include/rcssmin.py',
  336. 'ckan/include/rjsmin.py',
  337. 'ckan/lib/activity_streams.py',
  338. 'ckan/lib/activity_streams_session_extension.py',
  339. 'ckan/lib/alphabet_paginate.py',
  340. 'ckan/lib/app_globals.py',
  341. 'ckan/lib/captcha.py',
  342. 'ckan/lib/cli.py',
  343. 'ckan/lib/create_test_data.py',
  344. 'ckan/lib/dictization/__init__.py',
  345. 'ckan/lib/dictization/model_dictize.py',
  346. 'ckan/lib/dictization/model_save.py',
  347. 'ckan/lib/email_notifications.py',
  348. 'ckan/lib/extract.py',
  349. 'ckan/lib/fanstatic_extensions.py',
  350. 'ckan/lib/fanstatic_resources.py',
  351. 'ckan/lib/formatters.py',
  352. 'ckan/lib/hash.py',
  353. 'ckan/lib/help/flash_messages.py',
  354. 'ckan/lib/i18n.py',
  355. 'ckan/lib/jinja_extensions.py',
  356. 'ckan/lib/jsonp.py',
  357. 'ckan/lib/mailer.py',
  358. 'ckan/lib/maintain.py',
  359. 'ckan/lib/navl/dictization_functions.py',
  360. 'ckan/lib/navl/validators.py',
  361. 'ckan/lib/package_saver.py',
  362. 'ckan/lib/plugins.py',
  363. 'ckan/lib/render.py',
  364. 'ckan/lib/search/__init__.py',
  365. 'ckan/lib/search/index.py',
  366. 'ckan/lib/search/query.py',
  367. 'ckan/lib/search/sql.py',
  368. 'ckan/logic/__init__.py',
  369. 'ckan/logic/action/__init__.py',
  370. 'ckan/logic/action/delete.py',
  371. 'ckan/logic/action/get.py',
  372. 'ckan/logic/action/update.py',
  373. 'ckan/logic/auth/create.py',
  374. 'ckan/logic/auth/delete.py',
  375. 'ckan/logic/auth/get.py',
  376. 'ckan/logic/auth/update.py',
  377. 'ckan/logic/converters.py',
  378. 'ckan/logic/schema.py',
  379. 'ckan/logic/validators.py',
  380. 'ckan/migration/versions/001_add_existing_tables.py',
  381. 'ckan/migration/versions/002_add_author_and_maintainer.py',
  382. 'ckan/migration/versions/003_add_user_object.py',
  383. 'ckan/migration/versions/004_add_group_object.py',
  384. 'ckan/migration/versions/005_add_authorization_tables.py',
  385. 'ckan/migration/versions/006_add_ratings.py',
  386. 'ckan/migration/versions/007_add_system_roles.py',
  387. 'ckan/migration/versions/008_update_vdm_ids.py',
  388. 'ckan/migration/versions/009_add_creation_timestamps.py',
  389. 'ckan/migration/versions/010_add_user_about.py',
  390. 'ckan/migration/versions/011_add_package_search_vector.py',
  391. 'ckan/migration/versions/012_add_resources.py',
  392. 'ckan/migration/versions/013_add_hash.py',
  393. 'ckan/migration/versions/014_hash_2.py',
  394. 'ckan/migration/versions/015_remove_state_object.py',
  395. 'ckan/migration/versions/016_uuids_everywhere.py',
  396. 'ckan/migration/versions/017_add_pkg_relationships.py',
  397. 'ckan/migration/versions/018_adjust_licenses.py',
  398. 'ckan/migration/versions/019_pkg_relationships_state.py',
  399. 'ckan/migration/versions/020_add_changeset.py',
  400. 'ckan/migration/versions/022_add_group_extras.py',
  401. 'ckan/migration/versions/023_add_harvesting.py',
  402. 'ckan/migration/versions/024_add_harvested_document.py',
  403. 'ckan/migration/versions/025_add_authorization_groups.py',
  404. 'ckan/migration/versions/026_authorization_group_user_pk.py',
  405. 'ckan/migration/versions/027_adjust_harvester.py',
  406. 'ckan/migration/versions/028_drop_harvest_source_status.py',
  407. 'ckan/migration/versions/029_version_groups.py',
  408. 'ckan/migration/versions/030_additional_user_attributes.py',
  409. 'ckan/migration/versions/031_move_openid_to_new_field.py',
  410. 'ckan/migration/versions/032_add_extra_info_field_to_resources.py',
  411. 'ckan/migration/versions/033_auth_group_user_id_add_conditional.py',
  412. 'ckan/migration/versions/034_resource_group_table.py',
  413. 'ckan/migration/versions/035_harvesting_doc_versioning.py',
  414. 'ckan/migration/versions/036_lockdown_roles.py',
  415. 'ckan/migration/versions/037_role_anon_editor.py',
  416. 'ckan/migration/versions/038_delete_migration_tables.py',
  417. 'ckan/migration/versions/039_add_expired_id_and_dates.py',
  418. 'ckan/migration/versions/040_reset_key_on_user.py',
  419. 'ckan/migration/versions/041_resource_new_fields.py',
  420. 'ckan/migration/versions/042_user_revision_indexes.py',
  421. 'ckan/migration/versions/043_drop_postgres_search.py',
  422. 'ckan/migration/versions/044_add_task_status.py',
  423. 'ckan/migration/versions/045_user_name_unique.py',
  424. 'ckan/migration/versions/046_drop_changesets.py',
  425. 'ckan/migration/versions/047_rename_package_group_member.py',
  426. 'ckan/migration/versions/048_add_activity_streams_tables.py',
  427. 'ckan/migration/versions/049_add_group_approval_status.py',
  428. 'ckan/migration/versions/050_term_translation_table.py',
  429. 'ckan/migration/versions/051_add_tag_vocabulary.py',
  430. 'ckan/migration/versions/052_update_member_capacities.py',
  431. 'ckan/migration/versions/053_add_group_logo.py',
  432. 'ckan/migration/versions/054_add_resource_created_date.py',
  433. 'ckan/migration/versions/055_update_user_and_activity_detail.py',
  434. 'ckan/migration/versions/056_add_related_table.py',
  435. 'ckan/migration/versions/057_tracking.py',
  436. 'ckan/migration/versions/058_add_follower_tables.py',
  437. 'ckan/migration/versions/059_add_related_count_and_flag.py',
  438. 'ckan/migration/versions/060_add_system_info_table.py',
  439. 'ckan/migration/versions/061_add_follower__group_table.py',
  440. 'ckan/migration/versions/062_add_dashboard_table.py',
  441. 'ckan/migration/versions/063_org_changes.py',
  442. 'ckan/migration/versions/064_add_email_last_sent_column.py',
  443. 'ckan/migration/versions/065_add_email_notifications_preference.py',
  444. 'ckan/migration/versions/067_turn_extras_to_strings.py',
  445. 'ckan/misc.py',
  446. 'ckan/model/__init__.py',
  447. 'ckan/model/activity.py',
  448. 'ckan/model/authz.py',
  449. 'ckan/model/dashboard.py',
  450. 'ckan/model/domain_object.py',
  451. 'ckan/model/extension.py',
  452. 'ckan/model/follower.py',
  453. 'ckan/model/group.py',
  454. 'ckan/model/group_extra.py',
  455. 'ckan/model/license.py',
  456. 'ckan/model/meta.py',
  457. 'ckan/model/misc.py',
  458. 'ckan/model/modification.py',
  459. 'ckan/model/package.py',
  460. 'ckan/model/package_extra.py',
  461. 'ckan/model/package_relationship.py',
  462. 'ckan/model/rating.py',
  463. 'ckan/model/related.py',
  464. 'ckan/model/resource.py',
  465. 'ckan/model/system_info.py',
  466. 'ckan/model/tag.py',
  467. 'ckan/model/task_status.py',
  468. 'ckan/model/term_translation.py',
  469. 'ckan/model/test_user.py',
  470. 'ckan/model/tracking.py',
  471. 'ckan/model/types.py',
  472. 'ckan/model/user.py',
  473. 'ckan/model/vocabulary.py',
  474. 'ckan/authz.py',
  475. 'ckan/pastertemplates/__init__.py',
  476. 'ckan/plugins/interfaces.py',
  477. 'ckan/plugins/toolkit.py',
  478. 'ckan/poo.py',
  479. 'ckan/rating.py',
  480. 'ckan/templates_legacy/home/__init__.py',
  481. 'ckan/tests/legacy/__init__.py',
  482. 'ckan/tests/legacy/ckantestplugin/ckantestplugin/__init__.py',
  483. 'ckan/tests/legacy/ckantestplugin/setup.py',
  484. 'ckan/tests/legacy/ckantestplugins.py',
  485. 'ckan/tests/legacy/functional/api/base.py',
  486. 'ckan/tests/legacy/functional/api/model/test_group.py',
  487. 'ckan/tests/legacy/functional/api/model/test_licenses.py',
  488. 'ckan/tests/legacy/functional/api/model/test_package.py',
  489. 'ckan/tests/legacy/functional/api/model/test_ratings.py',
  490. 'ckan/tests/legacy/functional/api/model/test_relationships.py',
  491. 'ckan/tests/legacy/functional/api/model/test_revisions.py',
  492. 'ckan/tests/legacy/functional/api/model/test_tag.py',
  493. 'ckan/tests/legacy/functional/api/model/test_vocabulary.py',
  494. 'ckan/tests/legacy/functional/api/test_activity.py',
  495. 'ckan/tests/legacy/functional/api/test_api.py',
  496. 'ckan/tests/legacy/functional/api/test_dashboard.py',
  497. 'ckan/tests/legacy/functional/api/test_email_notifications.py',
  498. 'ckan/tests/legacy/functional/api/test_follow.py',
  499. 'ckan/tests/legacy/functional/api/test_misc.py',
  500. 'ckan/tests/legacy/functional/api/test_package_search.py',
  501. 'ckan/tests/legacy/functional/api/test_resource.py',
  502. 'ckan/tests/legacy/functional/api/test_resource_search.py',
  503. 'ckan/tests/legacy/functional/api/test_revision_search.py',
  504. 'ckan/tests/legacy/functional/api/test_user.py',
  505. 'ckan/tests/legacy/functional/api/test_util.py',
  506. 'ckan/tests/legacy/functional/base.py',
  507. 'ckan/tests/legacy/functional/test_activity.py',
  508. 'ckan/tests/legacy/functional/test_admin.py',
  509. 'ckan/tests/legacy/functional/test_cors.py',
  510. 'ckan/tests/legacy/functional/test_error.py',
  511. 'ckan/tests/legacy/functional/test_home.py',
  512. 'ckan/tests/legacy/functional/test_package.py',
  513. 'ckan/tests/legacy/functional/test_package_relationships.py',
  514. 'ckan/tests/legacy/functional/test_pagination.py',
  515. 'ckan/tests/legacy/functional/test_preview_interface.py',
  516. 'ckan/tests/legacy/functional/test_related.py',
  517. 'ckan/tests/legacy/functional/test_revision.py',
  518. 'ckan/tests/legacy/functional/test_search.py',
  519. 'ckan/tests/legacy/functional/test_tag.py',
  520. 'ckan/tests/legacy/functional/test_tag_vocab.py',
  521. 'ckan/tests/legacy/functional/test_upload.py',
  522. 'ckan/tests/legacy/functional/test_user.py',
  523. 'ckan/tests/legacy/html_check.py',
  524. 'ckan/tests/legacy/lib/__init__.py',
  525. 'ckan/tests/legacy/lib/test_accept.py',
  526. 'ckan/tests/legacy/lib/test_alphabet_pagination.py',
  527. 'ckan/tests/legacy/lib/test_cli.py',
  528. 'ckan/tests/legacy/lib/test_dictization.py',
  529. 'ckan/tests/legacy/lib/test_email_notifications.py',
  530. 'ckan/tests/legacy/lib/test_hash.py',
  531. 'ckan/tests/legacy/lib/test_helpers.py',
  532. 'ckan/tests/legacy/lib/test_i18n.py',
  533. 'ckan/tests/legacy/lib/test_mailer.py',
  534. 'ckan/tests/legacy/lib/test_munge.py',
  535. 'ckan/tests/legacy/lib/test_navl.py',
  536. 'ckan/tests/legacy/lib/test_resource_search.py',
  537. 'ckan/tests/legacy/lib/test_simple_search.py',
  538. 'ckan/tests/legacy/lib/test_solr_package_search.py',
  539. 'ckan/tests/legacy/lib/test_solr_package_search_synchronous_update.py',
  540. 'ckan/tests/legacy/lib/test_solr_schema_version.py',
  541. 'ckan/tests/legacy/lib/test_solr_search_index.py',
  542. 'ckan/tests/legacy/lib/test_tag_search.py',
  543. 'ckan/tests/legacy/logic/test_action.py',
  544. 'ckan/tests/legacy/logic/test_auth.py',
  545. 'ckan/tests/legacy/logic/test_tag.py',
  546. 'ckan/tests/legacy/logic/test_validators.py',
  547. 'ckan/tests/legacy/misc/test_format_text.py',
  548. 'ckan/tests/legacy/misc/test_mock_mail_server.py',
  549. 'ckan/tests/legacy/misc/test_sync.py',
  550. 'ckan/tests/legacy/mock_mail_server.py',
  551. 'ckan/tests/legacy/mock_plugin.py',
  552. 'ckan/tests/legacy/models/test_extras.py',
  553. 'ckan/tests/legacy/models/test_group.py',
  554. 'ckan/tests/legacy/models/test_license.py',
  555. 'ckan/tests/legacy/models/test_misc.py',
  556. 'ckan/tests/legacy/models/test_package.py',
  557. 'ckan/tests/legacy/models/test_package_relationships.py',
  558. 'ckan/tests/legacy/models/test_purge_revision.py',
  559. 'ckan/tests/legacy/models/test_resource.py',
  560. 'ckan/tests/legacy/models/test_revision.py',
  561. 'ckan/tests/legacy/models/test_user.py',
  562. 'ckan/tests/legacy/monkey.py',
  563. 'ckan/tests/legacy/pylons_controller.py',
  564. 'ckan/tests/legacy/schema/test_schema.py',
  565. 'ckan/tests/legacy/test_plugins.py',
  566. 'ckan/tests/legacy/test_versions.py',
  567. 'ckan/websetup.py',
  568. 'ckanext/datastore/bin/datastore_setup.py',
  569. 'ckanext/datastore/logic/action.py',
  570. 'ckanext/datastore/plugin.py',
  571. 'ckanext/datastore/tests/test_create.py',
  572. 'ckanext/datastore/tests/test_search.py',
  573. 'ckanext/datastore/tests/test_upsert.py',
  574. 'ckanext/example_idatasetform/plugin.py',
  575. 'ckanext/example_itemplatehelpers/plugin.py',
  576. 'ckanext/multilingual/plugin.py',
  577. 'ckanext/resourceproxy/plugin.py',
  578. 'ckanext/stats/controller.py',
  579. 'ckanext/stats/plugin.py',
  580. 'ckanext/stats/stats.py',
  581. 'ckanext/stats/tests/__init__.py',
  582. 'ckanext/stats/tests/test_stats_lib.py',
  583. 'ckanext/stats/tests/test_stats_plugin.py',
  584. 'ckanext/test_tag_vocab_plugin.py',
  585. 'ckanext/tests/plugin.py',
  586. 'doc/conf.py',
  587. 'fabfile.py',
  588. 'profile_tests.py',
  589. 'setup.py',
  590. ]
  591. fails = {}
  592. passes = []
  593. done = False
  594. @classmethod
  595. def setup(cls):
  596. if not cls.done:
  597. cls.process()
  598. cls.done = True
  599. @classmethod
  600. def process(cls):
  601. blacklist = cls.PEP8_BLACKLIST_FILES
  602. for path, filename in process_directory(base_path):
  603. errors = cls.find_pep8_errors(filename=path)
  604. if errors and not filename in blacklist:
  605. cls.fails[filename] = output_errors(filename, errors)
  606. elif not errors and filename in blacklist:
  607. cls.passes.append(filename)
  608. def test_pep8_fails(self):
  609. msg = 'The following files have pep8 issues that need resolving'
  610. msg += '\nThey need removing from the test blacklist'
  611. show_fails(msg, self.fails)
  612. def test_pep8_pass(self):
  613. msg = 'The following files passed pep8 but are blacklisted'
  614. show_passing(msg, self.passes)
  615. @classmethod
  616. def find_pep8_errors(cls, filename=None, lines=None):
  617. try:
  618. sys.stdout = cStringIO.StringIO()
  619. config = {}
  620. # Ignore long lines on test files, as the test names can get long
  621. # when following our test naming standards.
  622. if cls._is_test(filename):
  623. config['ignore'] = ['E501']
  624. checker = pep8.Checker(filename=filename, lines=lines,
  625. **config)
  626. checker.check_all()
  627. output = sys.stdout.getvalue()
  628. finally:
  629. sys.stdout = sys.__stdout__
  630. errors = []
  631. for line in output.split('\n'):
  632. parts = line.split(' ', 2)
  633. if len(parts) == 3:
  634. location, error, desc = parts
  635. line_no = location.split(':')[1]
  636. errors.append('%s ln:%s %s' % (error, line_no, desc))
  637. return errors
  638. @classmethod
  639. def _is_test(cls, filename):
  640. return bool(re.search('(^|\W)test_.*\.py$', filename, re.IGNORECASE))
  641. class TestActionAuth(object):
  642. ''' These tests check the logic auth/action functions are compliant. The
  643. main tests are that each action has a corresponding auth function and
  644. that each auth function has an action. We check the function only
  645. accepts (context, data_dict) as parameters. '''
  646. ACTION_FN_SIGNATURES_BLACKLIST = [
  647. 'create: activity_create',
  648. ]
  649. ACTION_NO_AUTH_BLACKLIST = [
  650. 'create: follow_dataset',
  651. 'create: follow_group',
  652. 'create: follow_user',
  653. 'create: package_relationship_create_rest',
  654. 'delete: package_relationship_delete_rest',
  655. 'delete: unfollow_dataset',
  656. 'delete: unfollow_group',
  657. 'delete: unfollow_user',
  658. 'get: activity_detail_list',
  659. 'get: am_following_dataset',
  660. 'get: am_following_group',
  661. 'get: am_following_user',
  662. 'get: dashboard_activity_list_html',
  663. 'get: dataset_followee_count',
  664. 'get: dataset_follower_count',
  665. 'get: followee_count',
  666. 'get: group_activity_list',
  667. 'get: group_activity_list_html',
  668. 'get: group_followee_count',
  669. 'get: group_follower_count',
  670. 'get: group_package_show',
  671. 'get: member_list',
  672. 'get: organization_activity_list',
  673. 'get: organization_activity_list_html',
  674. 'get: organization_follower_count',
  675. 'get: package_activity_list',
  676. 'get: package_activity_list_html',
  677. 'get: recently_changed_packages_activity_list',
  678. 'get: recently_changed_packages_activity_list_html',
  679. 'get: related_list',
  680. 'get: resource_search',
  681. 'get: roles_show',
  682. 'get: status_show',
  683. 'get: tag_search',
  684. 'get: term_translation_show',
  685. 'get: user_activity_list',
  686. 'get: user_activity_list_html',
  687. 'get: user_followee_count',
  688. 'get: user_follower_count',
  689. 'update: package_relationship_update_rest',
  690. 'update: task_status_update_many',
  691. 'update: term_translation_update_many',
  692. ]
  693. AUTH_NO_ACTION_BLACKLIST = [
  694. 'create: file_upload',
  695. 'delete: revision_delete',
  696. 'delete: revision_undelete',
  697. 'get: group_autocomplete',
  698. 'get: group_list_available',
  699. 'get: sysadmin',
  700. 'get: request_reset',
  701. 'get: user_reset',
  702. 'update: group_change_state',
  703. 'update: group_edit_permissions',
  704. 'update: package_change_state',
  705. 'update: revision_change_state',
  706. ]
  707. ACTION_NO_DOC_STR_BLACKLIST = [
  708. 'create: group_create_rest',
  709. 'create: package_create_rest',
  710. 'create: package_relationship_create_rest',
  711. 'delete: package_relationship_delete_rest',
  712. 'get: get_site_user',
  713. 'get: group_show_rest',
  714. 'get: package_show_rest',
  715. 'get: tag_show_rest',
  716. 'update: group_update_rest',
  717. 'update: package_relationship_update_rest',
  718. 'update: package_update_rest',
  719. ]
  720. done = False
  721. @classmethod
  722. def setup(cls):
  723. if not cls.done:
  724. cls.process()
  725. cls.done = True
  726. @classmethod
  727. def process(cls):
  728. def get_functions(module_root):
  729. fns = {}
  730. for auth_module_name in ['get', 'create', 'update', 'delete']:
  731. module_path = '%s.%s' % (module_root, auth_module_name,)
  732. try:
  733. module = __import__(module_path)
  734. except ImportError:
  735. print ('No auth module for action "%s"' % auth_module_name)
  736. for part in module_path.split('.')[1:]:
  737. module = getattr(module, part)
  738. for key, v in module.__dict__.items():
  739. if not hasattr(v, '__call__'):
  740. continue
  741. if v.__module__ != module_path:
  742. continue
  743. if not key.startswith('_'):
  744. name = '%s: %s' % (auth_module_name, key)
  745. fns[name] = v
  746. return fns
  747. cls.actions = get_functions('logic.action')
  748. cls.auths = get_functions('logic.auth')
  749. def test_actions_have_auth_fn(self):
  750. actions_no_auth = set(self.actions.keys()) - set(self.auths.keys())
  751. actions_no_auth -= set(self.ACTION_NO_AUTH_BLACKLIST)
  752. assert not actions_no_auth, 'These actions have no auth function\n%s' \
  753. % '\n'.join(sorted(list(actions_no_auth)))
  754. def test_actions_have_auth_fn_blacklist(self):
  755. actions_no_auth = set(self.actions.keys()) & set(self.auths.keys())
  756. actions_no_auth &= set(self.ACTION_NO_AUTH_BLACKLIST)
  757. assert not actions_no_auth, 'These actions blacklisted but ' + \
  758. 'shouldn\'t be \n%s' % '\n'.join(sorted(list(actions_no_auth)))
  759. def test_auths_have_action_fn(self):
  760. auths_no_action = set(self.auths.keys()) - set(self.actions.keys())
  761. auths_no_action -= set(self.AUTH_NO_ACTION_BLACKLIST)
  762. assert not auths_no_action, 'These auth functions have no action\n%s' \
  763. % '\n'.join(sorted(list(auths_no_action)))
  764. def test_auths_have_action_fn_blacklist(self):
  765. auths_no_action = set(self.auths.keys()) & set(self.actions.keys())
  766. auths_no_action &= set(self.AUTH_NO_ACTION_BLACKLIST)
  767. assert not auths_no_action, 'These auths functions blacklisted but' + \
  768. ' shouldn\'t be \n%s' % '\n'.join(sorted(list(auths_no_action)))
  769. def test_fn_signatures(self):
  770. errors = []
  771. for name, fn in self.actions.iteritems():
  772. args_info = inspect.getargspec(fn)
  773. if (args_info.args != ['context', 'data_dict']
  774. or args_info.varargs is not None
  775. or args_info.keywords is not None):
  776. if name not in self.ACTION_FN_SIGNATURES_BLACKLIST:
  777. errors.append(name)
  778. assert not errors, 'These action functions have the wrong function' + \
  779. ' signature, should be (context, data_dict)\n%s' \
  780. % '\n'.join(sorted(errors))
  781. def test_fn_docstrings(self):
  782. errors = []
  783. for name, fn in self.actions.iteritems():
  784. if not getattr(fn, '__doc__', None):
  785. if name not in self.ACTION_NO_DOC_STR_BLACKLIST:
  786. errors.append(name)
  787. assert not errors, 'These action functions need docstrings\n%s' \
  788. % '\n'.join(sorted(errors))
  789. class TestBadExceptions(object):
  790. ''' Look for a common coding problem in ckan Exception(_'...') '''
  791. # Exceptions should not on the whole be translated as they are for
  792. # programmers to read in trace backs or log files. However some like
  793. # Invalid used in validation functions do get passed back up to the user
  794. # and so should be translated.
  795. NASTY_EXCEPTION_BLACKLIST_FILES = [
  796. 'ckan/controllers/api.py',
  797. 'ckan/controllers/user.py',
  798. 'ckan/lib/mailer.py',
  799. 'ckan/logic/action/create.py',
  800. 'ckan/logic/action/delete.py',
  801. 'ckan/logic/action/get.py',
  802. 'ckan/logic/action/update.py',
  803. 'ckan/logic/auth/create.py',
  804. 'ckan/logic/auth/delete.py',
  805. 'ckan/logic/auth/get.py',
  806. 'ckan/authz.py',
  807. 'ckanext/datastore/logic/action.py',
  808. ]
  809. fails = {}
  810. passes = []
  811. done = False
  812. @classmethod
  813. def setup(cls):
  814. if not cls.done:
  815. cls.process()
  816. cls.done = True
  817. @classmethod
  818. def process(cls):
  819. blacklist = cls.NASTY_EXCEPTION_BLACKLIST_FILES
  820. re_nasty_exception = re.compile(
  821. r'''raise\W+(?![^I]*Invalid\().*_\('''
  822. )
  823. for path, filename in process_directory(base_path):
  824. f = open(path, 'r')
  825. count = 1
  826. errors = []
  827. for line in f:
  828. if re_nasty_exception.search(line):
  829. errors.append('ln:%s \t%s' % (count, line[:-1]))
  830. count += 1
  831. if errors and not filename in blacklist:
  832. cls.fails[filename] = output_errors(filename, errors)
  833. elif not errors and filename in blacklist:
  834. cls.passes.append(filename)
  835. def test_good(self):
  836. msg = 'The following files passed nasty exceptions rules'
  837. msg += '\nThey need removing from the test blacklist'
  838. show_passing(msg, self.passes)
  839. def test_bad(self):
  840. msg = ('The following files have nasty exception issues that need'
  841. ' resolving\nWe should not be translating exceptions in most'
  842. ' situations. We need to when the exception message is passed'
  843. ' to the front end for example validation')
  844. show_fails(msg, self.fails)