-
Notifications
You must be signed in to change notification settings - Fork 341
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
fix: Can't run with specified profile name and --keep-profile-changes #968
Conversation
the failure seems to be because of the title of the PR. |
Hi @harikishen Currently the tests are only failing on the PR title linting, nevertheless, most of the code from this PR is currently not covered by the tests (in other words: "it is currectly not tested"), as you can see from the coveralls failures and related reports, e.g.: By looking in a bit more details into the diff, the first thing that I notice is that you are currently trying to use finder.getPath('default') as a function that synchronously return a result, while it is actually a function that call a callback asynchronously and pass the result as a parameter to it. To make the code more readable I have probably already suggested you, in our last chat on IRC about this change, to use es6-promisify (which is already in the web-ext dependencies and we are currently using it to wrap the zip-dir library methods with promise-based wrapper functions), but it looks that we are not yet wrapping the finder object using es6-promisify (and we should also use To learn more about how to use es6-promisify, you can look at the project README here: To summarize the changes that are still needed on this PR to put it on the right direction (at least from my current point of view):
|
src/firefox/index.js
Outdated
@@ -251,7 +251,37 @@ export async function useProfile( | |||
customPrefs = {}, | |||
}: UseProfileParams = {}, | |||
): Promise<FirefoxProfile> { | |||
const profile = new FirefoxProfile({destinationDirectory: profilePath}); | |||
let profile; | |||
const finder = new FirefoxProfile.Finder(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we will need to be able to customize the path where the profiles will be searched by the profile Finder
helper class, to be able to more easily test these behaviors in the unit tests.
And so this function should take an additional searchProfilesPath
(or another similar identifier name) parameter which defaults to undefined
(so that the default behavior of the Finder class will be preserved when this method is executed without passing the search profile path explicitly).
src/firefox/index.js
Outdated
const profile = new FirefoxProfile({destinationDirectory: profilePath}); | ||
let profile; | ||
const finder = new FirefoxProfile.Finder(); | ||
const finderGetPath = promisify(finder.getPath.bind(finder)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finderGetPath
will reject with an Error
when the Finder
instance is not able to find the requested profile name, which is something that we should expect to happen (e.g. because the user could have no profile with that name yet).
And so, to be able to only call findGetPath
when we know that it will resolve to the path instead of being rejected with an Error, we can use the finder.readProfiles
method and check if the named profiles that we need actually exists before trying to resolve them into paths, e.g. something like the following snippet should do the trick:
...
const finder = new FirefoxProfile.Finder(searchProfilesPath);
const finderGetPath = promisify(finder.getPath, finder);
const finderReadProfiles = promisify(finder.readProfiles, finder);
// Read the profiles list from the profiles.ini file from `searchProfilesPath`.
await finderReadProfiles();
// Helper function which returns true if the profile name exists in the profiles.ini file.
const hasProfileName = (profileName) => {
return finder.profiles.filter(
(profileDef) => profileDef.Name === profileName
).length !== 0;
};
...
const defaultProfilePath = (
hasProfileName('default') && await finderGetPath('default')
);
const defaultDevProfilePath = (
hasProfileName('default') && await finderGetPath('dev-edition-default')
);
This way defaultProfilePath
and defaultDevProfilePath
will be the profile path (which is a string), or undefined
if the profile doesn't exist in the profiles.ini
list.
src/firefox/index.js
Outdated
if (dirExists) { | ||
log.debug(`Copying profile directory from "${profilePath}"`); | ||
const defaultProfilePath = await finderGetPath('default'); | ||
const defaultDevProfilePath = await finderGetPath('default-dev-edition'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default name of the Firefox DevEdition profile is 'dev-edition-default'
(instead of 'default-dev-edition'
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @harikishen
Here is some additional feedback (mostly related to the usage of the Finder
class).
I just took a deeper look into the Finder
class implementation from the "firefox-profile"
dependency and I added above some inline feedback comments about how we can check upfront if the default profiles names exists (before trying to resolve the profile name into profile paths using finder.getPath
).
@rpl could you look into it now ? |
i've written 1 more test for named profiles. |
@rpl coveralls fails because of hasProfileName(). How do I write a test for it ? |
src/firefox/index.js
Outdated
const finder = new FirefoxProfile.Finder(searchProfilesPath); | ||
const finderGetPath = promisify(finder.getPath, finder); | ||
const hasProfileName = (profileName) => { | ||
return finder.profiles.filter( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part was not currently covered in the tests because finder.profiles
is going to be an empty array if finder.readProfiles
has not been called first.
src/firefox/index.js
Outdated
}: UseProfileParams = {}, | ||
): Promise<FirefoxProfile> { | ||
const profile = new FirefoxProfile({destinationDirectory: profilePath}); | ||
let profile; | ||
const finder = new FirefoxProfile.Finder(searchProfilesPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given how we are using and testing the Finder
class, I'm wondering if it would be better to create an createProfileFinder
helper which creates the profile finder and wraps the methods that we need with promisify, something like
function defaultCreateProfileFinder() {
const finder = new FirefoxProfile.Finder();
const readProfiles = promisify(finder.readProfiles, finder);
return {
getPath: promisify(finder.getPath, finder),
hasProfileName: async () => {
await finder.readProfiles();
return finder.profiles.filter(
(profileDef) => profileDef.Name === profileName
).length !== 0;
}
}
}
So that we can simplify useProfile
:
export async function useProfile(
profilePath: string,
{
...
createProfileFinder: defaultCreateProfileFinder,
}: UseProfileParams = P{,
): Promise<FirefoxProfile> {
const finder = createProfileFinder();
...
}
and more easily test its behavior, e.g. because we can override createProfileFinder
with a function that returns a "fake" version of our profile finder wrapper (and so we dont' even need to create the real "profiles.ini" file or the fake profiles directory in tmpDir).
src/firefox/index.js
Outdated
await finderGetPath('dev-edition-default') | ||
); | ||
if (dirExists) { | ||
log.debug(`Copying profile directory from "${profilePath}"`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This message does not seem right here, useProfile
is never going to copy a profile.
src/firefox/index.js
Outdated
} | ||
profile = new FirefoxProfile({destinationDirectory: profilePath}); | ||
} else { | ||
log.debug(`Assuming ${profilePath} is a named profile`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should make this to always fail if the requested profile name is default
or dev-edition-default
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rpl what do you propose ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something like:
if (profilePath === 'default' || profilePath === 'dev-edition-default') {
throw new WebExtError(`Cannot use the blacklisted named profile "${profilePath}" `);
}
src/firefox/index.js
Outdated
} | ||
} catch (error) { | ||
throw new WebExtError( | ||
`Could not copy Firefox profile from ${profilePath}: ${error}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the message logged at line 275, this error message is also not the right one.
hasProfileName: () => Promise.resolve(true), | ||
}; | ||
}; | ||
const spy = sinon.spy(profileFinder, "getPath"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harikishen this is probably a leftover, but it is making the CI to fail on the eslint checks and it never reaches the unit tests:
sinon.spy((profile) => Promise.resolve(profile)); | ||
const app = 'fennec'; | ||
const profilePath = tmpDir.path(); | ||
const profileFinder = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harikishen profileFinder
is a function that creates the fake Finder
objects, and not a Finder
object itself, that is the reason why the assertion commented on line 351 is not working for you.
You should do something like:
const profileFinder = {
getPath: sinon.spy(...),
...
};
const createProfileFinder = () => profileFinder;
const profile = await firefox.useProfile(profilePath, {
...
configureThisProfile: configureProfile,
createProfileFinder: createProfileFinder,
});
assert.equal(configureProfile.called, true);
...
assert.equal(profileFinder.getPath.callCount, 2)
@rpl what do you think of the changes in index.js ? |
src/firefox/index.js
Outdated
hasProfileName: async (profileName: string) => { | ||
try { | ||
await fs.stat(path.join(FirefoxProfile.Finder.locateUserDirectory(), | ||
'profile.ini')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"profiles.ini"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I was trying the case if the file did not exist... forgot to fix it
src/firefox/index.js
Outdated
'profile.ini')); | ||
} catch (error) { | ||
if (isErrorWithCode('ENOENT', error)) { | ||
log.info('No firefox profiles exist'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, we have already checked that "profiles.ini" doesn't exist (which should probably be a warning instead of an info message), but we are going to execute await readProfiles()
at line 266, which will raises an exception because of that.
And so I think that it would be reasonable to return false
after logging a warning (no profile name can be found if no "profiles.ini"
has been found).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the call for warning...???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unsurprisingly log.warn
:-) (
Line 174 in 97404a9
log.warn(`Running stubbed function ${key} (default implementation)`); |
@rpl i've done the requested changes. What else remains to be done ? |
sinon.spy((profile) => Promise.resolve(profile)); | ||
const profileFinder = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be nicer if we can keep the same naming convention in all the tests, e.g. this profileFinder identifier is the function that creates the fake Finder instance in some of the tests and the fake Finder instance in other tests.
Also, if you name these identifier like the params property names, you can use the ES6 short syntax, eg.
const createProfileFinder = () => ...;
const configureThisProfile = () => ...;
...
return firefox.useProfile(profilePath, {app, configureThisProfile, createProfileFinder});
const app = 'fennec'; | ||
const profilePath = baseProfile.path(); | ||
return firefox.useProfile(profilePath, {app, configureThisProfile}) | ||
return firefox.useProfile(profilePath, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
most of these tests would be easier to read if we use the await
syntax here
const profile = await firefox.useProfile(...);
... // test assertions
src/firefox/index.js
Outdated
}; | ||
|
||
export function defaultCreateProfileFinder() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the new tests are using a fake createProfileFinder
, and so (like the code coverage reports confirm) it is time to create some tests specifically for defaultCreateProfileFinder).
To make this function more easily testable we should add an optional userDirectoryPath
(a string) parameter to it, so that we can customize the directory where the Finder
class will search the profiles.ini
file and the profile directories.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what tests do you propose ?
src/firefox/index.js
Outdated
log.debug(`Using profile directory from "${profilePath}"`); | ||
if (profilePath === defaultProfilePath || | ||
profilePath === defaultDevProfilePath) { | ||
throw new WebExtError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By looking at the coverage reports, this line doesn't seem to be currently executed by any of the current tests.
@rpl The issue seems to be with some sort of race condition with |
src/firefox/index.js
Outdated
getPath: promisify(finder.getPath, finder), | ||
hasProfileName: async (profileName: string) => { | ||
try { | ||
await fs.stat(path.join(FirefoxProfile.Finder.locateUserDirectory(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harikishen the tests are failing because on an issue in the hasProfileName
implementation:
- while we are configuring the Finder to use the specified parameter as the profiles search path (as expected)
at line 251, here we are always doing an fs.stat on the default search path theFirefoxProfile.Finder.locateUserDirectory()
provides (but it should use the same path that theFinder
is going to use
And so it should be something like:
const profilesIniPath = path.join(
userDirectoryPath || FirefoxProfile.Finder.locateUserDirectory(),
'profiles.ini'
);
const res = await fs.stat(profilesIniPath);
src/firefox/index.js
Outdated
}; | ||
|
||
export function defaultCreateProfileFinder(userDirectoryPath: string = '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be ?string
(a string that can be null) and it should not default to an empty string
(it should be undefined when not specified, and so we can just remove the explicit assigned default)
src/firefox/index.js
Outdated
@@ -99,6 +99,11 @@ export interface FirefoxProcess extends events$EventEmitter { | |||
kill: Function; | |||
} | |||
|
|||
export interface IProfileFinder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This flow type is currently unused and it should be used for the defaultCreateProfileFinder
return value type annotation (we should also move it near that function, that is where it is going to be used).
Also, the type signatures are wrong: both hasProfileName
and getPath
take a string as parameter
(and so they should be hasProfileName(string): Promise<boolean>
, and getPath
needs the same kind of fix).
src/firefox/index.js
Outdated
}; | ||
|
||
export function defaultCreateProfileFinder(userDirectoryPath: string = '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This defaultCreateProfileFinder
function should have a type annotation of the return value:
export function defaultCreateProfileFinder(userDirectoryPath: ?string): IProfileFinder {
...
}
Once the return value is "type annotated" correctly, flow will notice some new errors that needs to be fixed, in particular:
hasProfileName
needs the type annotation of the return value
hasProfileName: async (profileName: string): Promise<boolean> => {
...
},
- we should only catch the exception of
fs.stat
in thetry...catch
at line 256. - we should re-throw any exception that as been caught by the above
try...catch
which do not verify theisErrorWithCode('ENOENT', error)
check - in the test file
"test.firefox.js"
the fakegetPath
defined at lines 417 and 444 should resolve to a string value:
diff --git a/tests/unit/test-firefox/test.firefox.js b/tests/unit/test-firefox/test.firefox.js
index da6a612..caa54b8 100644
--- a/tests/unit/test-firefox/test.firefox.js
+++ b/tests/unit/test-firefox/test.firefox.js
@@ -414,7 +414,7 @@ describe('firefox', () => {
sinon.spy((profile) => Promise.resolve(profile));
const createProfileFinder = () => {
return {
- getPath: () => Promise.resolve(),
+ getPath: () => Promise.resolve('/fake/profilePath'),
hasProfileName: () => Promise.resolve(true),
};
};
@@ -441,7 +441,7 @@ describe('firefox', () => {
sinon.spy((profile) => Promise.resolve(profile));
const createProfileFinder = () => {
return {
- getPath: () => Promise.resolve(),
+ getPath: () => Promise.resolve('/fake/profilePath'),
hasProfileName: () => Promise.resolve(true),
};
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @harikishen
Follows a new round of review comments (mostly tweaks to simplify and clean up the patch a bit, and some fixes around the exceptions handling).
src/firefox/index.js
Outdated
`Cannot use profile at "${profilePath}"` | ||
); | ||
} | ||
profile = new FirefoxProfile({destinationDirectory: profilePath}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can make this function to call const profile = new FirefoxProfile(...)
in a single place by limiting this block and the one that contains line 317 to only "resolve the final destinationDirectory
value", and then creating the FirefoxProfile
instance at the end of this function.
src/firefox/index.js
Outdated
}; | ||
|
||
export function defaultCreateProfileFinder(userDirectoryPath?: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IProfileFinder
flow type defined at line 102 is still unused, it should be used as the return value of this function
(and we should also apply the related fixes suggested in my previous round of comments, but first we should take a look at the changes that I'm suggesting in one of the comments below, they can be helpful to simplify this helper as well as the useProfile
implementation before fixing this flow type checking
related issue).
src/firefox/index.js
Outdated
return finder.profiles.filter( | ||
(profileDef) => profileDef.Name === profileName).length !== 0; | ||
} catch (error) { | ||
if (isErrorWithCode('ENOENT', error)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This try catch should only surround the await fs.stat
call and it should re-throw any error that does not verify isErrorWithCode('ENOENT', error)
(or, on the contrary, this function is going to return undefined
on all the other kind of exception that can be caught from this try...catch
).
src/firefox/index.js
Outdated
try { | ||
const dirExists = await isDirectory(profilePath); | ||
if (await finder.hasProfileName('default')) { | ||
defaultProfilePath = await finder.getPath('default'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an alternative approach to this two if (await finder.hasProfileName(...)) { ... = await finder.getPath(...); }
we can opt to return from createProfileFinder
a single function that returns a string (the profile path) if there is such a profile name and undefined
(or null
) otherwise
(in other words: we can merge the hasProfileName
and getPath
function defined at line 253 into a single helper function with the behavior described above).
This change can make this method implementation cleaner and simpler, turning it into something like:
const getProfilePath = createProfileFinder();
const dirExists = await isDirectory(profilePath);
let destinationDirectory;
if (dirExists) {
...
if (profilePath === getProfilePath('default') ||
profilePath === getProfilePath('dev-edition-default')) {
throw new WebExtError(...);
}
destinationDirectory = profilePath;
} else {
...
if (profilePath === 'default' ||
profilePath === 'dev-edition-default') {
throw new WebExtError(...);
}
destinationDirectory = getProfilePath(profilePath);
if (!destinationDirectory) {
throw new UserError(
`The request "${profilePath}" profile name cannot be resolved to a profile path`
);
}
}
....
const profile = new FirefoxProfile({destinationDirectory});
return await configureThisProfile(profile, {app, customPrefs});
src/firefox/index.js
Outdated
const profileDirectory = await finder.getPath(profilePath); | ||
profile = new FirefoxProfile({destinationDirectory: profileDirectory}); | ||
} | ||
} catch (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that this giant try...catch
is actually useful, at least by looking currently we can just leave any other exception to be propagated to the callers.
@rpl I've done some of the changes mentioned. Please look into it and tell me whether i'm on the right track. |
@rpl I have a lot of doubts regarding rewriting the tests. So I've commented them for now. |
Hi @harikishen, sorry for the waiting time (I've been traveling for a conference), Commenting the tests that fail is worst than let them fail on travis, by leaving them to fail on travis I would be able to look at the failures and help you to figure out what to do about the failing tests. In general, the main point of writing tests is not to make them pass, on the contrary, it is to see them fail when something has been changed and needs more changes to be handled correctly, so the main point of writing tests is to read the failures logs ;-) |
Fixes #932
@rpl @kumar303 Is this enough ?