Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better exercise var name in tests #627

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
136 changes: 115 additions & 21 deletions dev/src/ExercismDev/ExercismExerciseGenerator.class.st
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
"
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,
#superclass : #Object,
#instVars : [
'numberGenerated',
'exerciseDirReference',
'testJson'
'testJson',
'regenerateExisting'
],
#classVars : [
'DefaultPath'
Expand Down Expand Up @@ -46,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 }
Expand All @@ -72,6 +85,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 [

Expand Down Expand Up @@ -111,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 [

Expand All @@ -123,6 +150,12 @@ ExercismExerciseGenerator >> exerciseTestAlreadyExists [
^ Smalltalk hasClassNamed: self testClassName
]

{ #category : #internal }
ExercismExerciseGenerator >> exerciseVariableName [

^ self testNameCamelCased asValidSelector asString
]

{ #category : #generation }
ExercismExerciseGenerator >> generateExerciseCommentFor: testClass [
| comment |
Expand All @@ -146,35 +179,39 @@ 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 ].

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.
self generateTestMethodsFor: testClass.
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!'
Expand All @@ -187,6 +224,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 ifTrue: [ ^ self ].

"compile method with uuid"
self generateUUIDMethodFor: testClass.

Expand All @@ -202,7 +242,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'
Expand All @@ -213,10 +253,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
]

Expand All @@ -235,7 +275,7 @@ ExercismExerciseGenerator >> generateTestMethodsFor: testClass [
testMethodGenerator
testClass: testClass;
testCaseJson: testCaseJson;
testVariable: self testVariableName;
testVariable: self exerciseVariableName;
testPrefix: '';
generateTests.
]
Expand Down Expand Up @@ -270,12 +310,23 @@ 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 [

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 }
Expand All @@ -301,6 +352,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 [

Expand Down Expand Up @@ -334,12 +434,6 @@ ExercismExerciseGenerator >> testNameCamelCased [

]

{ #category : #internal }
ExercismExerciseGenerator >> testVariableName [

^ (self testNameCamelCased, 'Calculator') asValidSelector asString
]

{ #category : #internal }
ExercismExerciseGenerator >> updateCategorisation [
"utility script to fix categorisations"
Expand Down
40 changes: 40 additions & 0 deletions dev/src/ExercismTests/ExercismExerciseGeneratorTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -143,3 +153,33 @@ 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|
"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'.
]
16 changes: 13 additions & 3 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -72,14 +72,22 @@ 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: <path-to-problem-specifications/exercises>` - 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).
- More specifically, you can generate test class for specific exercise by: `ExercismExerciseGenerator generateExerciseFrom: '<path-to-problem-specifications/exercises/slug-name>' asFileReference`.

Result of previous statement will be new `<SlugNameTest>` (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.

Expand All @@ -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 <name>". This will let others know you are working on one, and will also form a basis for your later pull request.

Expand Down