From 077ee1400095666f4c45ecb433ea41930de95622 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:07:48 +0200 Subject: [PATCH 01/11] Exercise generator can regenerate existing exercises. --- .../ExercismExerciseGenerator.class.st | 85 ++++++++++++++++--- .../ExercismExerciseGeneratorTest.class.st | 26 ++++++ 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st index ab8e14f3..4189a2b4 100644 --- a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st +++ b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st @@ -11,7 +11,8 @@ Class { #instVars : [ 'numberGenerated', 'exerciseDirReference', - 'testJson' + 'testJson', + 'regenerateExisting' ], #classVars : [ 'DefaultPath' @@ -72,6 +73,12 @@ ExercismExerciseGenerator class >> writeLegacyPackageBaselineNames [ show: '''Exercise@' , n , '''' ] ] +{ #category : #internal } +ExercismExerciseGenerator >> canOverwriteExisting [ + + ^ self regenerateExisting or: [self exerciseTestAlreadyExists not] +] + { #category : #internal } ExercismExerciseGenerator >> compile: src for: aClass selector: aSelector protocol: aName [ @@ -123,6 +130,12 @@ ExercismExerciseGenerator >> exerciseTestAlreadyExists [ ^ Smalltalk hasClassNamed: self testClassName ] +{ #category : #internal } +ExercismExerciseGenerator >> exerciseVariableName [ + + ^ self testNameCamelCased asValidSelector asString +] + { #category : #generation } ExercismExerciseGenerator >> generateExerciseCommentFor: testClass [ | comment | @@ -149,7 +162,9 @@ ExercismExerciseGenerator >> generateExerciseFrom: aFileSystemReference [ self exerciseIsDeprecated ifTrue: [ ^self log: 'is deprecated (skipping)' for: self testClassName ]. - self exerciseTestAlreadyExists ifTrue: [ ^self log: 'already exists (skipping)' for: self testClassName ]. + + self canOverwriteExisting + ifFalse: [ ^self log: 'exercise test class already exists (skipping).' for: self testClassName ]. testClass := self generateTestClass. self generateSetupFor: testClass. @@ -187,6 +202,9 @@ ExercismExerciseGenerator >> generateMetaDataFor: testClass [ "write commment with exercise info to class" self generateExerciseCommentFor: testClass. + "Create UUID and version, only if creating new exercise (not regenerating existing)" + self regenerateExisting ifFalse: [ ^ self ]. + "compile method with uuid" self generateUUIDMethodFor: testClass. @@ -202,7 +220,7 @@ ExercismExerciseGenerator >> generateSetupFor: testClass [ outStream << 'setUp'; cr; tab; << 'super setUp.'; cr; - tab; << self testVariableName; << ' := '; << self testNameCamelCased; << ' new'. + tab; << self exerciseVariableName; << ' := '; << self testNameCamelCased; << ' new'. ]. self compile: src for: testClass selector: #setUp protocol: 'running' @@ -213,10 +231,10 @@ ExercismExerciseGenerator >> generateSetupFor: testClass [ ExercismExerciseGenerator >> generateTestClass [ ^ ExercismTest << self testClassName asSymbol - slots: {self testVariableName asSymbol}; + slots: {self exerciseVariableName asSymbol}; sharedVariables: {}; tag: self testNameCamelCased; - package: 'ExercismWIP'; + package: self packageNameForTestClass; install ] @@ -235,7 +253,7 @@ ExercismExerciseGenerator >> generateTestMethodsFor: testClass [ testMethodGenerator testClass: testClass; testCaseJson: testCaseJson; - testVariable: self testVariableName; + testVariable: self exerciseVariableName; testPrefix: ''; generateTests. ] @@ -301,6 +319,55 @@ ExercismExerciseGenerator >> numberGenerated: anObject [ numberGenerated := anObject ] +{ #category : #internal } +ExercismExerciseGenerator >> packageNameForTestClass [ + + "if regenerating existing class, use exercise package like 'Exercise@SlugName', otherwise just WIP package" + self regenerateExisting ifTrue: [ + ^ 'Exercise@{1}' format: {self testNameCamelCased} + ]. + ^ self defaultPackageName +] + +{ #category : #generation } +ExercismExerciseGenerator >> regenerateExerciseFrom: aFileSystemReference [ + + self regenerateExisting: true. + self generateExerciseFrom: aFileSystemReference +] + +{ #category : #accessing } +ExercismExerciseGenerator >> regenerateExisting [ + + ^ regenerateExisting +] + +{ #category : #accessing } +ExercismExerciseGenerator >> regenerateExisting: aBool [ + + regenerateExisting := aBool +] + +{ #category : #generation } +ExercismExerciseGenerator >> regenerateExistingExercisesFrom: filePathReference [ + + "this will regenerate already existing exercises from problem specifications" + self traceCr: 'Regenerating existing TestCases from specification: ', filePathReference printString. + + self numberGenerated: 0. + ExercismExercise allExercises do: [:existingExercise | + filePathReference entries + do: [ :entry | + entry name = existingExercise name ifTrue: [ + self regenerateExerciseFrom: entry reference + ] + ] + ]. + self + traceCr: ('Existing exercises sucessfully regenerated with: {1} Tests!' + format: {self numberGenerated}) +] + { #category : #internal } ExercismExerciseGenerator >> testClassName [ @@ -334,12 +401,6 @@ ExercismExerciseGenerator >> testNameCamelCased [ ] -{ #category : #internal } -ExercismExerciseGenerator >> testVariableName [ - - ^ (self testNameCamelCased, 'Calculator') asValidSelector asString -] - { #category : #internal } ExercismExerciseGenerator >> updateCategorisation [ "utility script to fix categorisations" diff --git a/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st b/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st index 0fa1c27d..c5864279 100644 --- a/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st +++ b/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st @@ -106,6 +106,16 @@ ExercismExerciseGeneratorTest >> testAsValidKeywordOnString [ self assert: 'is <= 5' asValidKeyword equals: 'isLessThanOrEqualTo5' ] +{ #category : #tests } +ExercismExerciseGeneratorTest >> testExerciseVariableName [ + |generator| + "set directory reference with canonical description with tests of given exercise" + generator := ExercismExerciseGenerator new exerciseDirReference: + self createMockExerciseDirectory. + + self assert: generator exerciseVariableName equals: 'mockExercise'. +] + { #category : #tests } ExercismExerciseGeneratorTest >> testGenerateExerciseFrom [ |aPackage aClass testSelectors classSelectors| @@ -143,3 +153,19 @@ ExercismExerciseGeneratorTest >> testGenerateExerciseFrom [ ] + +{ #category : #tests } +ExercismExerciseGeneratorTest >> testPackageNameForTestClass [ + |generator| + "set directory reference with canonical description with tests of given exercise" + generator := ExercismExerciseGenerator new exerciseDirReference: + self createMockExerciseDirectory. + + "for new exercises, package should be set to work-in-progress" + generator regenerateExisting: false. + self assert: generator packageNameForTestClass equals: 'ExercismWIP'. + + "for existing exercises with solution already, it should be exercise package" + generator regenerateExisting: true. + self assert: generator packageNameForTestClass equals: 'Exercise@MockExercise'. +] From eb4e15ff33d755d6f614ada7d06fb17e0860ee1b Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:09:02 +0200 Subject: [PATCH 02/11] fixed initializaiton of regenerating flag, fixed condition to produce UUID and version methods --- dev/src/ExercismDev/ExercismExerciseGenerator.class.st | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st index 4189a2b4..14010016 100644 --- a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st +++ b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st @@ -203,7 +203,7 @@ ExercismExerciseGenerator >> generateMetaDataFor: testClass [ self generateExerciseCommentFor: testClass. "Create UUID and version, only if creating new exercise (not regenerating existing)" - self regenerateExisting ifFalse: [ ^ self ]. + self regenerateExisting ifTrue: [ ^ self ]. "compile method with uuid" self generateUUIDMethodFor: testClass. @@ -294,6 +294,9 @@ ExercismExerciseGenerator >> initialize [ super initialize. "reset number of generated test classes" self numberGenerated: 0. + + "by default don not regenerate existing exercises, new exercise generation is default" + self regenerateExisting: false. ] { #category : #internal } From 5bfdd5e0205b2c5148b8760cff08e52b2ee86891 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Thu, 9 May 2024 16:51:38 +0200 Subject: [PATCH 03/11] Update class documentation of exercism test generator. --- .../ExercismExerciseGenerator.class.st | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st index 14010016..e066855f 100644 --- a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st +++ b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st @@ -1,9 +1,21 @@ " -I am the source code generator for creating exercism compatible source files that can be checked into the exercism/pharo project for students to download. +I am the source code generator for creating test class with test cases that are generated out of problem specifications. Generated tests are then used for exercism compatible source files that can be checked into the exercism/pharo project for students to download. You need to have checked out the exercism problem-specifications to point the generator to, to get the test case definitions. -To try: self generate +## Generate new exercises +To prompt user with choosing directory with problem specifications and generate: +`ExercismExerciseGenerator generate` + +To generate individual exercise from problem specification: +`ExercismExerciseGenerator new generateExerciseFrom: 'path-to-problem-specifications/exercises/exercise-slug' asFileReference` + +## Regenerating existing exercises +Use only when need to update tests from problem specifications, existing class will be used in existing exercise package, only test methods will be overwritten (UUID and other methods will remain same). +`ExercismExerciseGenerator new regenerateExistingExercisesFrom: 'path-to-problem-specifications/exercises' asFileReference` + +When working just on specific exercise - its update: +`ExercismExerciseGenerator new regenerateExerciseFrom: 'path-to-problem-specifications/exercises/exercise-slug' asFileReference` " Class { #name : #ExercismExerciseGenerator, From 3181ef05641c55de123069693967e0368770d394 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Thu, 9 May 2024 17:17:41 +0200 Subject: [PATCH 04/11] Update contributing guideline --- docs/CONTRIBUTING.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e7323798..ac5d8e4e 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -21,7 +21,7 @@ To begin, you need to ensure that you have a complete Exercism development envir ### __Pharo image and IDE__ You need to have a Pharo development environment (running Pharo image - IDE) for creating the actual coding examples. -1. Use [PharoLauncher](https://pharo.org/download) to create a fresh 10.0 (stable) development image from `Official distributions` category, and launch it (you can also use [zerconf](https://get.pharo.org/) if you are familiar with it). +1. Use [PharoLauncher](https://pharo.org/download) to create a fresh stable development image from `Official distributions` category (as of 05/24 stable image is: Pharo 12.0 - 64bit (stable)), and launch it (you can also use [zerconf](https://get.pharo.org/) if you are familiar with it). 2. Launch your image to test everything runs ok. > __Note__: If you have any TIMEOUT problems refer to the [user installation instructions](./docs/INSTALLATION.md). @@ -72,7 +72,9 @@ Steps to complete exercise: ![Workflow to complete Practise exercise](/docs/images/overview-exercism-pharo-contribution.svg) ### __1. Create new Practise exercise__ -__From problem repository__ +*TLDR: All details/API description related to exercise generation can be found in class comment of `ExercismExerciseGenerator` class.* + +__New exercises from problem repository__ - If you want to start completely new Practise exercise (step 1a.), you can use problem specification repository and generate test class for given exercise by running: `ExercismExerciseGenerator generateFrom: ` - this will generate test classes for all exercises in problem specifications repository in `ExerciseWIP` package. > __Note__: You can use menu item in Pharo image for achieving same (World menu -> Exercism -> Generate test cases). @@ -80,6 +82,12 @@ __From problem repository__ Result of previous statement will be new `` (a subclass of ExercismTest) test class with generated test methods in `ExerciseWIP` package. +__Regenerate existing exercise from problem repository__ + +If you want to regenerate test methods of existing exercise (defined already in solution package - e.g. `Exercise@TwoFer`), you can use following code. Note that other methods will not be touched (e.g. uuid, version, etc.) + + `ExercismExerciseGenerator new regenerateExerciseFrom: 'path-to-problem-specifications/exercises/exercise-slug' asFileReference` + __From scratch__ If you have an interesting idea, refer to the documentation about adding new exercises. @@ -96,13 +104,15 @@ Note that: - Do not commit any configuration files or directories inside the exercise (this may be reviewed for future exercises, let us know if it becomes a problem). - Be sure to generate a new UUID for the exercise using `UUIDGenerator next`, and place that value in the `uuid` method for the test. +> __Note__: Current Pharo UUID generator is unfortunatelly not compliant with RFC 4122 - version 4. Therefore UUID generated from configlet should be used by evaluating: `/bin/configlet uuid` + ### __2. Work on existing exercise__ While there many ways to help, by far the easiest and most useful contribution is to complete a solution for any of the currently "open" exercise. * Ensure your image is caught up to the exercism/pharo-smalltalk main (and push any changes back to your fork) - * The exercises are all TestCases that been automatically generated from the aforementioned problem-specifications repository. You will find them as subclasses of ExercismTest in the ExercismWIP package. + * The exercises are all TestCases that been automatically generated from the aforementioned problem-specifications repository. You will find them as subclasses of ExercismTest in the ExercismWIP package. Note that regenerated ExercismTest might be already in given exercise package, if solution was already in place. ExercismWIP package is used for the new or work-in-progress exercises that weren't deployed yet alongside with solution class. * Once you have selected an Exercise you want to work on, create an Issue in Github specifying "Convert Exercise ". This will let others know you are working on one, and will also form a basis for your later pull request. From c61991ed311679a203d451a4fc24a9a4d54c71c8 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Wed, 22 May 2024 19:35:55 +0200 Subject: [PATCH 05/11] Make exercise generator more robust by testing required content of exercise dir. --- .../ExercismExerciseGenerator.class.st | 32 +++++++++++++++---- .../ExercismExerciseGeneratorTest.class.st | 14 ++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st index e066855f..70f08dd1 100644 --- a/dev/src/ExercismDev/ExercismExerciseGenerator.class.st +++ b/dev/src/ExercismDev/ExercismExerciseGenerator.class.st @@ -59,7 +59,7 @@ ExercismExerciseGenerator class >> generate [ chooseDirectory: 'Select the /exercises location in a full Exercism/problem-specifications git project' path: self defaultPath. - path ifNotNil: [ self new generateFrom: (self defaultPath: path) ] + path ifNotNil: [ self new generateFrom: path ] ] { #category : #examples } @@ -130,6 +130,14 @@ ExercismExerciseGenerator >> exerciseDirReference: anObject [ exerciseDirReference := anObject ] +{ #category : #generation } +ExercismExerciseGenerator >> exerciseDirectoriesDo: exerciseGenerateBlock [ + + self class defaultPath entries + select: #isDirectory + thenDo: [:dirEntry | exerciseGenerateBlock value: dirEntry ] +] + { #category : #internal } ExercismExerciseGenerator >> exerciseIsDeprecated [ @@ -171,6 +179,8 @@ ExercismExerciseGenerator >> generateExerciseFrom: aFileSystemReference [ "this is needed, from exercise directory all artefacts will be obtained" self exerciseDirReference: aFileSystemReference. + self hasValidTestDecriptions ifFalse: [^ self log: 'does not contain any test descriptions (skipping).' for: aFileSystemReference basename ]. + self exerciseIsDeprecated ifTrue: [ ^self log: 'is deprecated (skipping)' for: self testClassName ]. @@ -184,24 +194,24 @@ ExercismExerciseGenerator >> generateExerciseFrom: aFileSystemReference [ self generateMetaDataFor: testClass. self numberGenerated: self numberGenerated + 1. - self log: 'successfully created' for: self testClassName + self log: 'successfully created.' for: self testClassName - - - ] { #category : #generation } ExercismExerciseGenerator >> generateFrom: filePathReference [ + "set default path from parameter" + self class defaultPath: filePathReference. + "create WIP package for exercises, if missing" self ensureCreateExerciseWIPPackage. self traceCr: 'Generating new TestCases from specification: ', filePathReference printString. self numberGenerated: 0. - filePathReference entries - do: [ :entry | self generateExerciseFrom: entry reference ]. + + self exerciseDirectoriesDo: [:dirEntry | self generateExerciseFrom: dirEntry reference ]. self traceCr: ('Generation complete. Created {1} Tests!' @@ -300,6 +310,14 @@ ExercismExerciseGenerator >> generateVersionMethodFor: testClass [ ] +{ #category : #generation } +ExercismExerciseGenerator >> hasValidTestDecriptions [ + + "answer true, if directory with problem description contains valid test description - must contain file canonical-data.json" + + ^ self exerciseDirReference fileNames includes: 'canonical-data.json' +] + { #category : #initialization } ExercismExerciseGenerator >> initialize [ diff --git a/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st b/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st index c5864279..9b2eefb1 100644 --- a/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st +++ b/dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st @@ -154,6 +154,20 @@ ExercismExerciseGeneratorTest >> testGenerateExerciseFrom [ ] +{ #category : #tests } +ExercismExerciseGeneratorTest >> testHasValidTestDecriptions [ + |generator| + "set directory reference with canonical description with tests of given exercise" + generator := ExercismExerciseGenerator new exerciseDirReference: + self createMockExerciseDirectory. + + self assert: generator hasValidTestDecriptions. + + "now remove test descriptions" + (generator exerciseDirReference / 'canonical-data.json') ensureDelete. + self deny: generator hasValidTestDecriptions. +] + { #category : #tests } ExercismExerciseGeneratorTest >> testPackageNameForTestClass [ |generator| From a702bd433c44890406f322eb8f5408b88c452a0c Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Wed, 29 May 2024 15:29:14 +0200 Subject: [PATCH 06/11] Refactored exercise source code generator to work just for given exercise (not all). --- .../ExercismDev/ExercismGenerator.class.st | 126 +++++++++++++----- 1 file changed, 95 insertions(+), 31 deletions(-) diff --git a/dev/src/ExercismDev/ExercismGenerator.class.st b/dev/src/ExercismDev/ExercismGenerator.class.st index 060fe0a1..23791c19 100644 --- a/dev/src/ExercismDev/ExercismGenerator.class.st +++ b/dev/src/ExercismDev/ExercismGenerator.class.st @@ -55,7 +55,8 @@ Class { 'codeExporter', 'exercismExercise', 'exercisesPath', - 'osSubProcess' + 'osSubProcess', + 'updateConfig' ], #classVars : [ 'DefaultPath' @@ -125,6 +126,13 @@ ExercismGenerator >> createTagSnapshotFor: packageOrTag [ [ :mc | mc className isNil or: [ mc actualClass category endsWith: packageOrTag name ] ]) ] +{ #category : #helper } +ExercismGenerator >> exerciseDirReferenceFrom: exerciseName [ + "returns dir reference to exercise dir - e.g. /exercises/two-fer/" + + ^ self exercisesPath asFileReference / exerciseName +] + { #category : #accessing } ExercismGenerator >> exercisesPath [ @@ -192,6 +200,31 @@ ExercismGenerator >> generateCustomDataFor: anExercismExercise to: destinationDi lf ]] ] +{ #category : #generation } +ExercismGenerator >> generateForExercise: slugName [ + + | exerciseToGenerate | + exerciseToGenerate := self exercismExercise + find: slugName + ifAbsent: [ self error: ('Exercise {1} not found.' format: { slugName }) ]. + + "generate source files for given exercise" + self generateSourceFilesFor: exerciseToGenerate exercisePackage. + + "generate configuration config.json file" + ExercismConfigGenerator generateTo: self basePathReference. + + "run configlet generation" + self runConfigletCommand +] + +{ #category : #helper } +ExercismGenerator >> generateReadmeHintAndCustomDataOf: testClass to: metaDirectoryRef [ + + self generateReadmeHintFor: testClass exercise to: metaDirectoryRef. + testClass isCustom ifTrue: [ self generateCustomDataFor: testClass exercise to: metaDirectoryRef ] +] + { #category : #helper } ExercismGenerator >> generateReadmeHintFor: anExercismExercise to: destinationDirectory [ "Generate markdown hints, that exercism configlet will pickup for readme.md files @@ -208,51 +241,35 @@ ExercismGenerator >> generateReadmeHintFor: anExercismExercise to: destinationDi ] { #category : #helper } -ExercismGenerator >> generateSourceFilesFor: packageOrTag to: filePathString [ +ExercismGenerator >> generateSourceFilesFor: packageOrTag [ "Generate the Tonel source files for a package (normally a tag). Answer the exercise directory reference" - | exampleDirectoryRef exerciseDirectoryRef metaDirectoryRef solutionDirectoryRef testClass testClassFilename exerciseName testClasses | - - "Note: could create the writer on a memory stream to then pick what should be stored on disk - e.g. - mem := FileSystem memory root. - writer := ExTonelWriter on: mem." + | exerciseDirectoryRef metaDirectoryRef solutionDirectoryRef testClass exerciseName testClasses | exerciseName := ExercismExercise exerciseNameFrom: packageOrTag. - exampleDirectoryRef := filePathString asFileReference. - exerciseDirectoryRef := exampleDirectoryRef / exerciseName. + exerciseDirectoryRef := self exerciseDirReferenceFrom: exerciseName. metaDirectoryRef := exerciseDirectoryRef / '.meta'. solutionDirectoryRef := metaDirectoryRef / 'solution'. - exerciseDirectoryRef ensureCreateDirectory. - exerciseDirectoryRef deleteAll. + self prepareExerciseDirectory: exerciseDirectoryRef. codeExporter directoryReference: solutionDirectoryRef; writeSnapshot: (self createTagSnapshotFor: packageOrTag). "move files to root solution directory and remove unnecessary package dir" - ((solutionDirectoryRef / packageOrTag name) allChildrenMatching: '*.st') do: [:aFile | - aFile moveTo: solutionDirectoryRef - ]. - (solutionDirectoryRef / packageOrTag name) delete. - - "Remove the package file as its not needed for Exercism" - (solutionDirectoryRef / 'package.st') delete. + self moveSolutionFilesOfPackage: packageOrTag to: solutionDirectoryRef. "Move the test file down to the exerciseDirectory" - testClasses := packageOrTag classes select: [ :cls | cls superclass = ExercismTest ]. - testClasses do: [ :tc | - testClassFilename := tc name, '.class.st'. - (solutionDirectoryRef / testClassFilename) moveTo: exerciseDirectoryRef / testClassFilename ]. + testClasses := self testClassesOfPackage: packageOrTag. + self moveTestClasses: testClasses from: solutionDirectoryRef to: exerciseDirectoryRef. - testClass := testClasses detect: [ :tc | tc class includesSelector: #exercise ]. - self generateReadmeHintFor: testClass exercise to: metaDirectoryRef. + "Generate readme hint and custom metadata" + testClass := testClasses detect: [ :tc | tc class includesSelector: #exercise ]. + self generateReadmeHintAndCustomDataOf: testClass to: metaDirectoryRef. - testClass isCustom ifTrue: [ self generateCustomDataFor: testClass exercise to: metaDirectoryRef ]. + ^exerciseDirectoryRef - ^exerciseDirectoryRef - ] { #category : #generation } @@ -261,9 +278,31 @@ ExercismGenerator >> generateSourceFilesForActiveExercises [ self exercismExercise allExercises select: [ :ex | ex isActive ] thenDo: [ :ex | - self - generateSourceFilesFor: ex exercisePackage - to: self exercisesPath ] + self generateSourceFilesFor: ex exercisePackage + ] +] + +{ #category : #helper } +ExercismGenerator >> moveSolutionFilesOfPackage: packageOrTag to: solutionDirectoryRef [ + + "move files to root solution directory and remove unnecessary package dir" + ((solutionDirectoryRef / packageOrTag name) allChildrenMatching: '*.st') do: [:aFile | + aFile moveTo: solutionDirectoryRef + ]. + (solutionDirectoryRef / packageOrTag name) delete. + + "Remove the package file as its not needed for Exercism" + (solutionDirectoryRef / 'package.st') delete +] + +{ #category : #helper } +ExercismGenerator >> moveTestClasses: aClasses from: solutionDirectoryRef to: exerciseDirectoryRef [ + + aClasses do: [ :tc | + |testClassFilename| + testClassFilename := '{1}.class.st' format: {tc name}. + (solutionDirectoryRef / testClassFilename) moveTo: exerciseDirectoryRef / testClassFilename + ] ] { #category : #accessing } @@ -282,6 +321,13 @@ ExercismGenerator >> osSubProcess: anOsSubProcess [ osSubProcess := anOsSubProcess ] +{ #category : #helper } +ExercismGenerator >> prepareExerciseDirectory: exerciseDirReference [ + + exerciseDirReference ensureCreateDirectory. + exerciseDirReference deleteAll. +] + { #category : #generation } ExercismGenerator >> runConfigletCommand [ |result | @@ -294,3 +340,21 @@ ExercismGenerator >> runConfigletCommand [ result isSuccess ifFalse: [ self error: 'failure running "configlet generate" - ' , result lastError printString ] ] + +{ #category : #helper } +ExercismGenerator >> testClassesOfPackage: packageOrTag [ + + ^ packageOrTag classes select: [ :cls | cls superclass = ExercismTest ] +] + +{ #category : #accessing } +ExercismGenerator >> updateConfig [ + + ^ updateConfig +] + +{ #category : #accessing } +ExercismGenerator >> updateConfig: aBool [ + + updateConfig := aBool +] From 0bae754efc70eefbf4ce36623744e77bf2d59043 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Tue, 11 Jun 2024 12:12:28 +0200 Subject: [PATCH 07/11] Added checking and fetching of configlet, set repository path as default, updated corresponding tests --- .../ExercismDev/ExercismGenerator.class.st | 40 ++++++++++++- dev/src/ExercismTests/DiskStore.extension.st | 10 ++++ .../ExercismGeneratorTest.class.st | 60 +++++++++++++++++++ .../ExercismTests/MockTestOSProcess.class.st | 5 ++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 dev/src/ExercismTests/DiskStore.extension.st diff --git a/dev/src/ExercismDev/ExercismGenerator.class.st b/dev/src/ExercismDev/ExercismGenerator.class.st index 23791c19..6e666e78 100644 --- a/dev/src/ExercismDev/ExercismGenerator.class.st +++ b/dev/src/ExercismDev/ExercismGenerator.class.st @@ -66,7 +66,10 @@ Class { { #category : #helper } ExercismGenerator class >> defaultPath [ - ^ DefaultPath ifNil: [ self defaultPath: FileLocator home pathString] + + ^ DefaultPath ifNil: [ + self defaultPath: (self exercismRepositoryPath ifNil: [FileLocator home pathString]) + ] ] { #category : #helper } @@ -74,6 +77,14 @@ ExercismGenerator class >> defaultPath: pathString [ ^ DefaultPath := pathString ] +{ #category : #helper } +ExercismGenerator class >> exercismRepositoryPath [ + + |repository| + repository := (IceRepository repositoryNamed: 'pharo-smalltalk') ifNil: [ self traceCr: 'Could not determine location of local Pharo Exercism repository.'. ^ nil ]. + ^ repository location pathString +] + { #category : #generation } ExercismGenerator class >> generate [ "This is the entry point for generating exercism compatible source files that can be checked into @@ -107,6 +118,15 @@ ExercismGenerator >> basePathReference [ ^ self exercisesPath parent ] +{ #category : #helper } +ExercismGenerator >> checkOrUpdateConfiglet [ + + (self class exercismRepositoryPath asFileReference / 'bin' / 'configlet') asFileReference isFile + ifTrue: [ ^ self ]. + self traceCr: 'Could not find configlet. Fetching configlet...'. + self fetchConfiglet +] + { #category : #accessing } ExercismGenerator >> codeExporter: anObject [ @@ -162,6 +182,22 @@ ExercismGenerator >> exercismExercise: anExerciseClass [ exercismExercise := anExerciseClass ] +{ #category : #helper } +ExercismGenerator >> fetchConfiglet [ + + |result | + result := self osSubProcess new + workingDirectory: self class exercismRepositoryPath; + command: 'bin/fetch-configlet'; + arguments: #(); + redirectStdout; + runAndWait. + + result isSuccess ifFalse: [ + self error: 'Cannot fetch configlet - ' , result lastError printString ]. + self traceCr: 'Configlet fetching - done!' +] + { #category : #generation } ExercismGenerator >> generate [ @@ -246,7 +282,7 @@ ExercismGenerator >> generateSourceFilesFor: packageOrTag [ | exerciseDirectoryRef metaDirectoryRef solutionDirectoryRef testClass exerciseName testClasses | - exerciseName := ExercismExercise exerciseNameFrom: packageOrTag. + exerciseName := self exercismExercise exerciseNameFrom: packageOrTag. exerciseDirectoryRef := self exerciseDirReferenceFrom: exerciseName. metaDirectoryRef := exerciseDirectoryRef / '.meta'. solutionDirectoryRef := metaDirectoryRef / 'solution'. diff --git a/dev/src/ExercismTests/DiskStore.extension.st b/dev/src/ExercismTests/DiskStore.extension.st new file mode 100644 index 00000000..e13cba90 --- /dev/null +++ b/dev/src/ExercismTests/DiskStore.extension.st @@ -0,0 +1,10 @@ +Extension { #name : #DiskStore } + +{ #category : #'*ExercismTests' } +DiskStore class >> currentFileSystem: fileSystem during: aBlock [ + | backupFileSystem | + backupFileSystem := self currentFileSystem. + [ CurrentFS := fileSystem. + aBlock value ] + ensure: [ CurrentFS:= backupFileSystem ] +] diff --git a/dev/src/ExercismTests/ExercismGeneratorTest.class.st b/dev/src/ExercismTests/ExercismGeneratorTest.class.st index 0e6d1ab7..efb8086a 100644 --- a/dev/src/ExercismTests/ExercismGeneratorTest.class.st +++ b/dev/src/ExercismTests/ExercismGeneratorTest.class.st @@ -168,6 +168,35 @@ ExercismGeneratorTest >> tearDown [ super tearDown ] +{ #category : #tests } +ExercismGeneratorTest >> testCheckOrUpdateConfiglet [ + |memFileSystem| + instance osSubProcess: FailedTestOSProcess. + + "memory file system should be used instead of system for testing purposes" + memFileSystem := instance exercisesPath fileSystem. + DiskStore + currentFileSystem: memFileSystem + during: [ + "when configlet not present - should proceed to fetching and should fail" + self should: [instance checkOrUpdateConfiglet] + raise: Error + whoseDescriptionIncludes: 'Cannot fetch configlet' + description: 'Fetching of configlet should success.'. + ]. + + DiskStore + currentFileSystem: memFileSystem + during: [ + (instance class exercismRepositoryPath asFileReference / 'bin' / 'configlet') ensureCreateFile. + "when configlet is present (fake file above) - it should not proceed to fetching" + self shouldnt: [instance checkOrUpdateConfiglet] + raise: Error + whoseDescriptionIncludes: 'Cannot fetch configlet' + description: 'Checking of configlet should pass and not proceed to fetching.'. + ] +] + { #category : #tests } ExercismGeneratorTest >> testFailedGenerateSignalsException [ @@ -178,6 +207,25 @@ ExercismGeneratorTest >> testFailedGenerateSignalsException [ description: 'Did not signal an error after succesful generation' ] +{ #category : #tests } +ExercismGeneratorTest >> testFetchConfiglet [ + + instance osSubProcess: SuccessfulTestOSProcess. + + self shouldnt: [instance fetchConfiglet] + raise: Error + whoseDescriptionIncludes: 'Cannot fetch configlet' + description: 'Fetching of configlet should success.'. + + instance osSubProcess: FailedTestOSProcess. + + self should: [instance fetchConfiglet] + raise: Error + whoseDescriptionIncludes: 'Cannot fetch configlet' + description: 'Fetching of configlet should fail.'. + +] + { #category : #tests } ExercismGeneratorTest >> testGenerate [ @@ -190,6 +238,18 @@ ExercismGeneratorTest >> testGenerate [ self assertSolution ] +{ #category : #tests } +ExercismGeneratorTest >> testGenerateForExercise [ + + instance osSubProcess: SuccessfulTestOSProcess. + + instance generateForExercise: 'two-fer'. + + self assertExerciseTests. + self assertHints. + self assertSolution +] + { #category : #tests } ExercismGeneratorTest >> testGenerateSourceFilesForActiveExercises [ diff --git a/dev/src/ExercismTests/MockTestOSProcess.class.st b/dev/src/ExercismTests/MockTestOSProcess.class.st index ee8b0789..c1f11896 100644 --- a/dev/src/ExercismTests/MockTestOSProcess.class.st +++ b/dev/src/ExercismTests/MockTestOSProcess.class.st @@ -39,3 +39,8 @@ MockTestOSProcess >> runAndWait [ "do nothing" ^ self ] + +{ #category : #mocking } +MockTestOSProcess >> workingDirectory: pathString [ + "do nothing" +] From b9dede8d8c93e737483fd0d759df40ec167d1e46 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:04:38 +0200 Subject: [PATCH 08/11] improved test description for fetching configlet --- dev/src/ExercismTests/ExercismGeneratorTest.class.st | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/src/ExercismTests/ExercismGeneratorTest.class.st b/dev/src/ExercismTests/ExercismGeneratorTest.class.st index efb8086a..0b9522af 100644 --- a/dev/src/ExercismTests/ExercismGeneratorTest.class.st +++ b/dev/src/ExercismTests/ExercismGeneratorTest.class.st @@ -182,7 +182,7 @@ ExercismGeneratorTest >> testCheckOrUpdateConfiglet [ self should: [instance checkOrUpdateConfiglet] raise: Error whoseDescriptionIncludes: 'Cannot fetch configlet' - description: 'Fetching of configlet should success.'. + description: 'Checking should proceed to fetching of configlet and should fail as exepected result of FailedTestOSProcess.'. ]. DiskStore From a81a6a27288ff501e8dc560eefdbb7877f34f99f Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:28:31 +0200 Subject: [PATCH 09/11] Use default root of configlet directory from git repository. If not present, use parent of exercises dir. --- dev/src/ExercismDev/ExercismGenerator.class.st | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dev/src/ExercismDev/ExercismGenerator.class.st b/dev/src/ExercismDev/ExercismGenerator.class.st index 6e666e78..edb68cf3 100644 --- a/dev/src/ExercismDev/ExercismGenerator.class.st +++ b/dev/src/ExercismDev/ExercismGenerator.class.st @@ -121,7 +121,7 @@ ExercismGenerator >> basePathReference [ { #category : #helper } ExercismGenerator >> checkOrUpdateConfiglet [ - (self class exercismRepositoryPath asFileReference / 'bin' / 'configlet') asFileReference isFile + (self configletRootReference / 'bin' / 'configlet') asFileReference isFile ifTrue: [ ^ self ]. self traceCr: 'Could not find configlet. Fetching configlet...'. self fetchConfiglet @@ -133,6 +133,16 @@ ExercismGenerator >> codeExporter: anObject [ codeExporter := anObject ] +{ #category : #accessing } +ExercismGenerator >> configletRootReference [ + + |configletPath| + "use default root of from git repository. If not present, use parent of exercises dir." + configletPath := self class exercismRepositoryPath. + configletPath ifNil: [ ^ self basePathReference ]. + ^ configletPath asFileReference +] + { #category : #helper } ExercismGenerator >> createTagSnapshotFor: packageOrTag [ | parentSnapshot | @@ -187,7 +197,7 @@ ExercismGenerator >> fetchConfiglet [ |result | result := self osSubProcess new - workingDirectory: self class exercismRepositoryPath; + workingDirectory: self configletRootReference fullName; command: 'bin/fetch-configlet'; arguments: #(); redirectStdout; From 4c78c5a8a3bec011e0f2706dde03e150c8c2b7d3 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:27:57 +0200 Subject: [PATCH 10/11] Fixed path in failing test. --- dev/src/ExercismTests/ExercismGeneratorTest.class.st | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/src/ExercismTests/ExercismGeneratorTest.class.st b/dev/src/ExercismTests/ExercismGeneratorTest.class.st index 0b9522af..c69701aa 100644 --- a/dev/src/ExercismTests/ExercismGeneratorTest.class.st +++ b/dev/src/ExercismTests/ExercismGeneratorTest.class.st @@ -188,7 +188,7 @@ ExercismGeneratorTest >> testCheckOrUpdateConfiglet [ DiskStore currentFileSystem: memFileSystem during: [ - (instance class exercismRepositoryPath asFileReference / 'bin' / 'configlet') ensureCreateFile. + (instance configletRootReference / 'bin' / 'configlet') ensureCreateFile. "when configlet is present (fake file above) - it should not proceed to fetching" self shouldnt: [instance checkOrUpdateConfiglet] raise: Error From 81d92a5d8fe39d06969f5eb9aaf940d2a3197e55 Mon Sep 17 00:00:00 2001 From: David Bajger <45875448+Bajger@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:27:33 +0200 Subject: [PATCH 11/11] Updated configlet metadata update, tests update, change in referencing root directory of configlet --- .../ExercismDev/ExercismGenerator.class.st | 58 ++++++++++++++----- .../ExercismGeneratorTest.class.st | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/dev/src/ExercismDev/ExercismGenerator.class.st b/dev/src/ExercismDev/ExercismGenerator.class.st index edb68cf3..383e249d 100644 --- a/dev/src/ExercismDev/ExercismGenerator.class.st +++ b/dev/src/ExercismDev/ExercismGenerator.class.st @@ -121,7 +121,7 @@ ExercismGenerator >> basePathReference [ { #category : #helper } ExercismGenerator >> checkOrUpdateConfiglet [ - (self configletRootReference / 'bin' / 'configlet') asFileReference isFile + (self configletRootReference / 'configlet') isFile ifTrue: [ ^ self ]. self traceCr: 'Could not find configlet. Fetching configlet...'. self fetchConfiglet @@ -137,10 +137,10 @@ ExercismGenerator >> codeExporter: anObject [ ExercismGenerator >> configletRootReference [ |configletPath| - "use default root of from git repository. If not present, use parent of exercises dir." + "use default bin directory where configlet is locatated (out from git repository). If not present, use parent of exercises dir." configletPath := self class exercismRepositoryPath. - configletPath ifNil: [ ^ self basePathReference ]. - ^ configletPath asFileReference + configletPath ifNil: [ ^ self basePathReference / 'bin' ]. + ^ configletPath asFileReference / 'bin' ] { #category : #helper } @@ -198,7 +198,7 @@ ExercismGenerator >> fetchConfiglet [ |result | result := self osSubProcess new workingDirectory: self configletRootReference fullName; - command: 'bin/fetch-configlet'; + command: 'fetch-configlet'; arguments: #(); redirectStdout; runAndWait. @@ -210,7 +210,8 @@ ExercismGenerator >> fetchConfiglet [ { #category : #generation } ExercismGenerator >> generate [ - + "this will regenerate all active exercises" + "generate source files for active exercises" self generateSourceFilesForActiveExercises. @@ -218,7 +219,7 @@ ExercismGenerator >> generate [ ExercismConfigGenerator generateTo: self basePathReference. "run configlet generation" - self runConfigletCommand. + self runConfigletCommandsForActiveExercices. ] { #category : #helper } @@ -260,8 +261,9 @@ ExercismGenerator >> generateForExercise: slugName [ "generate configuration config.json file" ExercismConfigGenerator generateTo: self basePathReference. - "run configlet generation" - self runConfigletCommand + "run configlet commands for given exercise" + self runTestSpecificationsUpdateForExercise: slugName. + self runDocsAndMetadataUpdateForExercise: slugName. ] { #category : #helper } @@ -375,16 +377,44 @@ ExercismGenerator >> prepareExerciseDirectory: exerciseDirReference [ ] { #category : #generation } -ExercismGenerator >> runConfigletCommand [ - |result | +ExercismGenerator >> runConfigletCommandsForActiveExercices [ + + self exercismExercise allExercises + select: [ :ex | ex isActive ] + thenDo: [ :ex | + self runTestSpecificationsUpdateForExercise: ex name. + self runDocsAndMetadataUpdateForExercise: ex name. + ] +] + +{ #category : #generation } +ExercismGenerator >> runDocsAndMetadataUpdateForExercise: slugName [ + | result | + result := self osSubProcess new - command: 'configlet generate'; - arguments: (Array with: self basePathReference pathString surroundedBySingleQuotes); + command: 'configlet'; + workingDirectory: self configletRootReference fullName; + arguments: {'sync'. '--docs'. '--filepaths'. '--metadata'. '-uy'. '-e'. slugName.} asArray; + redirectStdout; + runAndWait. + + result isSuccess ifFalse: [ + self error: ('Failed running "configlet sync --docs --filepaths --metadata -uy -e {1}" with result: {2}' format: { slugName. result lastError printString.})] +] + +{ #category : #generation } +ExercismGenerator >> runTestSpecificationsUpdateForExercise: slugName [ + | result | + + result := self osSubProcess new + command: 'configlet'; + workingDirectory: self configletRootReference fullName; + arguments: {'sync'. '-u'. '--tests'. 'include'. '-e'. slugName.} asArray; redirectStdout; runAndWait. result isSuccess ifFalse: [ - self error: 'failure running "configlet generate" - ' , result lastError printString ] + self error: ('Failed running "configlet sync -u --tests include -e {1}" with result: {2}' format: { slugName. result lastError printString.})] ] { #category : #helper } diff --git a/dev/src/ExercismTests/ExercismGeneratorTest.class.st b/dev/src/ExercismTests/ExercismGeneratorTest.class.st index c69701aa..e2af66a9 100644 --- a/dev/src/ExercismTests/ExercismGeneratorTest.class.st +++ b/dev/src/ExercismTests/ExercismGeneratorTest.class.st @@ -188,7 +188,7 @@ ExercismGeneratorTest >> testCheckOrUpdateConfiglet [ DiskStore currentFileSystem: memFileSystem during: [ - (instance configletRootReference / 'bin' / 'configlet') ensureCreateFile. + (instance configletRootReference / 'configlet') ensureCreateFile. "when configlet is present (fake file above) - it should not proceed to fetching" self shouldnt: [instance checkOrUpdateConfiglet] raise: Error